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 --- bar/ActiveWindow.qml | 30 +++++--- bar/Bar.qml | 24 ++++-- bar/Cpu.qml | 27 +++++++ bar/Gpu.qml | 27 +++++++ bar/Memory.qml | 27 +++++++ bar/Mpris.qml | 4 +- bar/Network.qml | 34 ++++++++ bar/Temp.qml | 27 +++++++ config/Bar.qml | 5 ++ config/General.qml | 6 ++ config/Icons.qml | 34 ++++++++ services/Mpris.qml | 188 +++++++++++++++++++++++++++++++++++++++++++++ services/Network.qml | 86 +++++++++++++++++++++ services/Resource.qml | 105 +++++++++++++++++++++++++ services/SMpris.qml | 188 --------------------------------------------- shell.qml | 5 +- utils/CircularProgress.qml | 91 ++++++++++++++++++++++ utils/Icons.qml | 34 ++++++++ utils/NetworkUsage.qml | 66 ++++++++++++++++ utils/Resource.qml | 59 ++++++++++++++ utils/networkUsage.js | 41 ++++++++++ 21 files changed, 903 insertions(+), 205 deletions(-) create mode 100644 bar/Cpu.qml create mode 100644 bar/Gpu.qml create mode 100644 bar/Memory.qml create mode 100644 bar/Network.qml create mode 100644 bar/Temp.qml create mode 100644 services/Mpris.qml create mode 100644 services/Network.qml create mode 100644 services/Resource.qml delete mode 100644 services/SMpris.qml create mode 100644 utils/CircularProgress.qml create mode 100644 utils/Icons.qml create mode 100644 utils/NetworkUsage.qml create mode 100644 utils/Resource.qml create mode 100644 utils/networkUsage.js diff --git a/bar/ActiveWindow.qml b/bar/ActiveWindow.qml index efdbfd9..4cb1a08 100644 --- a/bar/ActiveWindow.qml +++ b/bar/ActiveWindow.qml @@ -1,23 +1,35 @@ import QtQuick +import QtQuick.Layouts import Quickshell import Quickshell.Wayland import Quickshell.Widgets import qs.utils +import qs.config Item { id: root property var toplevel: ToplevelManager.activeToplevel property var icon: Quickshell.iconPath(AppSearch.guessIcon(toplevel?.appId), "image-missing") + anchors.verticalCenter: parent.verticalCenter - IconImage { - source: root.icon - implicitWidth: 32 - implicitHeight: 32 - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 8 + Rectangle { + anchors.fill: parent + color: "blue" } - Text { - text: root.toplevel?.title + RowLayout { + anchors.fill: parent + + anchors.verticalCenter: parent.verticalCenter + IconImage { + source: root.icon + implicitWidth: Bar.appIconSize + implicitHeight: Bar.appIconSize + Layout.fillHeight: true + Layout.fillWidth: false + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + } + Text { + text: root.toplevel?.title + } } } diff --git a/bar/Bar.qml b/bar/Bar.qml index f8d3a2d..24fc0ce 100644 --- a/bar/Bar.qml +++ b/bar/Bar.qml @@ -65,7 +65,7 @@ Scope { Layout.preferredWidth: 1 } } - RowLayout { + Item { id: centerRow Layout.fillWidth: true Layout.fillHeight: true @@ -76,11 +76,25 @@ Scope { id: rightRow Layout.fillWidth: true Layout.preferredWidth: 1 - Rectangle { + Cpu { + Layout.fillHeight: true + Layout.fillWidth: true + } + Memory { + Layout.fillHeight: true + Layout.fillWidth: true + } + Gpu { + Layout.fillHeight: true + Layout.fillWidth: true + } + Temp { + Layout.fillHeight: true + Layout.fillWidth: true + } + Network { + Layout.fillHeight: true Layout.fillWidth: true - Text { - text: "a" - } } } } diff --git a/bar/Cpu.qml b/bar/Cpu.qml new file mode 100644 index 0000000..5d65415 --- /dev/null +++ b/bar/Cpu.qml @@ -0,0 +1,27 @@ +import QtQuick +import QtQuick.Layouts + +import qs.services as Services +import qs.utils +import qs.config as Config + +Item { + id: root + RowLayout { + Item { + Layout.fillWidth: true + } + anchors.fill: parent + spacing: Config.Bar.resourceIconTextSpacing + + Icons { + text: Config.Icons.resource.cpu + } + Text { + text: Math.round(Services.Resource.cpuUsage * 100) + } + Item { + Layout.fillWidth: true + } + } +} diff --git a/bar/Gpu.qml b/bar/Gpu.qml new file mode 100644 index 0000000..e47d8f8 --- /dev/null +++ b/bar/Gpu.qml @@ -0,0 +1,27 @@ +import QtQuick +import QtQuick.Layouts + +import qs.services as Services +import qs.utils +import qs.config as Config + +Item { + id: root + RowLayout { + Item { + Layout.fillWidth: true + } + anchors.fill: parent + spacing: Config.Bar.resourceIconTextSpacing + + Icons { + text: Config.Icons.resource.gpu + } + Text { + text: Services.Resource.gpuUsage + } + Item { + Layout.fillWidth: true + } + } +} diff --git a/bar/Memory.qml b/bar/Memory.qml new file mode 100644 index 0000000..130c9ea --- /dev/null +++ b/bar/Memory.qml @@ -0,0 +1,27 @@ +import QtQuick +import QtQuick.Layouts + +import qs.services as Services +import qs.utils +import qs.config as Config + +Item { + id: root + RowLayout { + Item { + Layout.fillWidth: true + } + anchors.fill: parent + spacing: Config.Bar.resourceIconTextSpacing + + Icons { + text: Config.Icons.resource.memory + } + Text { + text: Math.round(Services.Resource.memoryUsedPercentage * 100) + "%" + } + Item { + Layout.fillWidth: true + } + } +} diff --git a/bar/Mpris.qml b/bar/Mpris.qml index 87bef93..8d56335 100644 --- a/bar/Mpris.qml +++ b/bar/Mpris.qml @@ -2,7 +2,7 @@ import QtQuick.Layouts import QtQuick import Quickshell import Quickshell.Services.Mpris -import qs.services +import qs.services as Services import qs.config // import Quickshell.Utils @@ -10,7 +10,7 @@ import qs.config Item { id: root height: Bar.height - readonly property MprisPlayer activePlayer: SMpris.activePlayer + readonly property MprisPlayer activePlayer: Services.Mpris.activePlayer readonly property string cleanedTitle: cleanMusicTitle(activePlayer?.trackTitle) || qsTr("No media") // length broken (https://github.com/quickshell-mirror/quickshell/issues/109) readonly property string position: friendlyTimeForSeconds(activePlayer?.position) || "0:00" diff --git a/bar/Network.qml b/bar/Network.qml new file mode 100644 index 0000000..df70ecd --- /dev/null +++ b/bar/Network.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Layouts + +import qs.services as Services +import qs.utils +import qs.config as Config + +Item { + id: root + + RowLayout { + Item { + Layout.fillWidth: true + } + anchors.fill: parent + spacing: Config.Bar.resourceIconTextSpacing + + Icons { + text: Services.Network.state + } + Text { + text: Math.round(Services.Resource.cpuUsage * 100) + } + Text { + text: NetworkUsage.up + " " + NetworkUsage.upUnit + } + Text { + text: NetworkUsage.down + " " + NetworkUsage.downUnit + } + Item { + Layout.fillWidth: true + } + } +} diff --git a/bar/Temp.qml b/bar/Temp.qml new file mode 100644 index 0000000..c2c5f1d --- /dev/null +++ b/bar/Temp.qml @@ -0,0 +1,27 @@ +import QtQuick +import QtQuick.Layouts + +import qs.services as Services +import qs.utils +import qs.config as Config + +Item { + id: root + RowLayout { + Item { + Layout.fillWidth: true + } + anchors.fill: parent + spacing: Config.Bar.resourceIconTextSpacing + + Icons { + text: Config.Icons.resource.temp + } + Text { + text: Math.round(Services.Resource.cpuTemp * 100) + } + Item { + Layout.fillWidth: true + } + } +} diff --git a/config/Bar.qml b/config/Bar.qml index 5a5be3a..8e14851 100644 --- a/config/Bar.qml +++ b/config/Bar.qml @@ -11,5 +11,10 @@ Singleton { readonly property int leftMargin: Style.spacing.larger readonly property int rightMargin: Style.spacing.larger + readonly property int workspaceIconSize: 32 readonly property int workspaces: 10 + + readonly property int appIconSize: 32 + + readonly property int resourceIconTextSpacing: 8 } diff --git a/config/General.qml b/config/General.qml index 80df622..2b71635 100644 --- a/config/General.qml +++ b/config/General.qml @@ -6,4 +6,10 @@ Singleton { id: root readonly property bool sloopySearch: false + + readonly property int resourceUpdateInterval: 1 + + readonly property real iconSize: Style.font.size.small ?? 16 + + readonly property string cpuThermalPath: "/sys/class/thermal/thermal_zone0/temp" } diff --git a/config/Icons.qml b/config/Icons.qml index ec52e80..13deee9 100644 --- a/config/Icons.qml +++ b/config/Icons.qml @@ -29,4 +29,38 @@ Singleton { mute: "", unmute: "" }) + + readonly property var wifi: ({ + min: "󰤯", + low: "󰤟", + mid: "󰤢", + high: "󰤥", + max: "󰤨" + }) + + readonly property var network: ({ + disconnected: "󰤭", + download: "", + upload: "", + wifi: wifi + }) + + readonly property var resource: ({ + cpu: "", + memory: "󰍛", + disk: "", + gpu: "󱓞", + temp: "", + network: network, + clock: "", + calendar: "" + }) + + readonly property var power: ({ + shutdown: "", + dpms: "󰶐", + lock: "󰍁", + suspend: "󰤄", + restart: "" + }) } 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(); - } - } -} diff --git a/shell.qml b/shell.qml index a2059bc..0997775 100644 --- a/shell.qml +++ b/shell.qml @@ -1,6 +1,9 @@ import Quickshell import "bar" +import QtQuick +import QtQuick.Layouts + Scope { - Bar {} + Bar {} } diff --git a/utils/CircularProgress.qml b/utils/CircularProgress.qml new file mode 100644 index 0000000..240e520 --- /dev/null +++ b/utils/CircularProgress.qml @@ -0,0 +1,91 @@ +// From https://github.com/rafzby/circular-progressbar with modifications +// License: LGPL-3.0 - A copy can be found in `licenses` folder of repo + +import QtQuick + +/** + * Material 3 circular progress. See https://m3.material.io/components/progress-indicators/specs + */ +Item { + id: root + + property int size: 30 + property int lineWidth: 2 + property real value: 0 + // property color primaryColor: Appearance.m3colors.m3onSecondaryContainer + // property color secondaryColor: Appearance.colors.colSecondaryContainer + property real gapAngle: Math.PI / 9 + property bool fill: false + property int fillOverflow: 2 + property int animationDuration: 1000 + property var easingType: Easing.OutCubic + + width: size + height: size + + signal animationFinished + + onValueChanged: { + canvas.degree = value * 360; + } + // onPrimaryColorChanged: { + // canvas.requestPaint(); + // } + // onSecondaryColorChanged: { + // canvas.requestPaint(); + // } + + Canvas { + id: canvas + + property real degree: 0 + + anchors.fill: parent + antialiasing: true + + onDegreeChanged: { + requestPaint(); + } + + onPaint: { + var ctx = getContext("2d"); + var x = root.width / 2; + var y = root.height / 2; + var radius = root.size / 2 - root.lineWidth; + var startAngle = (Math.PI / 180) * 270; + var fullAngle = (Math.PI / 180) * (270 + 360); + var progressAngle = (Math.PI / 180) * (270 + degree); + var epsilon = 0.01; // Small angle in radians + + ctx.reset(); + if (root.fill) { + // ctx.fillStyle = root.secondaryColor; + ctx.beginPath(); + ctx.arc(x, y, radius + fillOverflow, startAngle, fullAngle); + ctx.fill(); + } + ctx.lineCap = 'round'; + ctx.lineWidth = root.lineWidth; + + // Secondary + ctx.beginPath(); + ctx.arc(x, y, radius, progressAngle + gapAngle, fullAngle - gapAngle); + // ctx.strokeStyle = root.secondaryColor; + ctx.stroke(); + + // Primary (value indication) + var endAngle = progressAngle + (value > 0 ? 0 : epsilon); + ctx.beginPath(); + ctx.arc(x, y, radius, startAngle, endAngle); + // ctx.strokeStyle = root.primaryColor; + ctx.stroke(); + } + + Behavior on degree { + NumberAnimation { + duration: root.animationDuration + easing.type: root.easingType + } + } + } +} diff --git a/utils/Icons.qml b/utils/Icons.qml new file mode 100644 index 0000000..4b3915c --- /dev/null +++ b/utils/Icons.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Layouts + +import qs.config + +Text { + id: root + property real iconSize: General.iconSize + property real fill: 0 + property real truncatedFill: Math.round(fill * 100) / 100 // Reduce memory consumption spikes from constant font remapping + renderType: Text.NativeRendering + font { + hintingPreference: Font.PreferFullHinting + family: Style.font.family.symbols ?? "Symbols Nerd Font" + pixelSize: iconSize + weight: Font.Normal + (Font.DemiBold - Font.Normal) * fill + variableAxes: { + "FILL": truncatedFill, + // "wght": font.weight, + // "GRAD": 0, + "opsz": iconSize + } + } + verticalAlignment: Text.AlignVCenter + // color: Style.m3colors.m3onBackground + + // Behavior on fill { + // NumberAnimation { + // duration: Appearance?.animation.elementMoveFast.duration ?? 200 + // easing.type: Appearance?.animation.elementMoveFast.type ?? Easing.BezierSpline + // easing.bezierCurve: Appearance?.animation.elementMoveFast.bezierCurve ?? [0.34, 0.80, 0.34, 1.00, 1, 1] + // } + // } +} diff --git a/utils/NetworkUsage.qml b/utils/NetworkUsage.qml new file mode 100644 index 0000000..f71e3b7 --- /dev/null +++ b/utils/NetworkUsage.qml @@ -0,0 +1,66 @@ +pragma Singleton +// pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Io +import QtQuick +import "networkUsage.js" as Network +import qs.config + +Singleton { + property int up: 0 + property string upUnit: "" + property int down: 0 + property string downUnit: "" + + property string __iface: "" + property var __prev: ({ + rx: 0, + tx: 0 + }) + + Timer { + interval: General.resourceUpdateInterval * 1000 + running: true + repeat: true + onTriggered: update() + } + + function update() { + __iface = Network.defaultInterface(getRouteProcess.stdout.text); + const cur = Network.getRxTxBytes(getNetDevProcess.stdout.text, __iface); + const sec = General.resourceUpdateInterval; + const rawUp = (cur.tx - __prev.tx) / sec; + const rawDown = (cur.rx - __prev.rx) / sec; + + up = Network.formatSize(rawUp, false); + upUnit = Network.formatSize(rawUp, true); + down = Network.formatSize(rawDown, false); + downUnit = Network.formatSize(rawDown, true); + + __prev = cur; + getRouteProcess.running = true; + getNetDevProcess.running = true; + } + + Process { + id: getRouteProcess + running: true + command: ["ip", "route"] + stdout: StdioCollector {} + } + + Process { + id: getNetDevProcess + running: true + command: ["cat", "/proc/net/dev"] + stdout: StdioCollector {} + } + + Component.onCompleted: { + getRouteProcess.running = true; + getNetDevProcess.running = true; + __iface = Network.defaultInterface(getRouteProcess.stdout.text); + __prev = Network.getRxTxBytes(getNetDevProcess.stdout.text, __iface); + } +} diff --git a/utils/Resource.qml b/utils/Resource.qml new file mode 100644 index 0000000..2048e98 --- /dev/null +++ b/utils/Resource.qml @@ -0,0 +1,59 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io + +import qs.config + +Item { + id: root + + required property string iconName + required property double percentage + property bool shown: true + clip: true + visible: width > 0 && height > 0 + implicitWidth: resourceRowLayout.x < 0 ? 0 : childrenRect.width + implicitHeight: childrenRect.height + + RowLayout { + id: resourceRowLayout + spacing: 4 + x: root.shown ? 0 : -resourceRowLayout.width + + CircularProgress { + Layout.alignment: Qt.AlignVCenter + lineWidth: 2 + value: root.percentage + size: 26 + // secondaryColor: Appearance.colors.colSecondaryContainer + // primaryColor: Appearance.m3colors.m3onSecondaryContainer + + MaterialSymbol { + anchors.centerIn: parent + fill: 1 + text: iconName + // iconSize: Appearance.font.pixelSize.normal + // color: Appearance.m3colors.m3onSecondaryContainer + } + } + + Text { + Layout.alignment: Qt.AlignVCenter + // color: Appearance.colors.colOnLayer1 + text: `${Math.round(percentage * 100)}` + } + + Behavior on x { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + } + + Behavior on implicitWidth { + NumberAnimation { + duration: Appearance.animation.elementMove.duration + easing.type: Appearance.animation.elementMove.type + easing.bezierCurve: Appearance.animation.elementMove.bezierCurve + } + } +} diff --git a/utils/networkUsage.js b/utils/networkUsage.js new file mode 100644 index 0000000..a7131f2 --- /dev/null +++ b/utils/networkUsage.js @@ -0,0 +1,41 @@ +function defaultInterface(route) { + var lines = route.split(/\r?\n/) + for (var i = 0; i < lines.length; i++) { + var l = lines[i] + if (l.indexOf("default via") === 0) return l.split(/\s+/)[4] + } + throw "No default interface" +} +function getRxTxBytes(dev, iface) { + var lines = dev.split(/\r?\n/) + for (var i = 0; i < lines.length; i++) { + var l = lines[i].trim() + if (!l.startsWith(iface + ":")) continue + var p = l.split(/\s+/) + return { rx: parseInt(p[1]), tx: parseInt(p[9]) } + } + throw "Interface not found" +} +function formatSize(size, unit) { + const labels = ["B", "K", "M", "G", "T"] + let power = 0, kilo = 2 ** 10 + while (size > kilo && power < labels.length - 1) { + size /= kilo; power++ + } + if (unit) { + return labels[power] + } else { + return size.toFixed(0) + } +} + +// const iface = defaultInterface() +// let prev = getRxTxBytes(iface) + +// setInterval(() => { +// const cur = getRxTxBytes(iface) +// const dr = (cur.rx - prev.rx) / (interval / 1000) +// const dt = (cur.tx - prev.tx) / (interval / 1000) +// prev = cur +// console.log(formatSize(dr), formatSize(dt)) +// }, interval) -- cgit v1.2.3