From c4a018e39c3d40b43d088c470bfc2fbbba0412d0 Mon Sep 17 00:00:00 2001 From: Hannes Date: Mon, 15 Jun 2026 23:19:07 +0200 Subject: [PATCH] added beta of hover texts --- config/Config.qml | 1 + modules/bar/states/Clock/ClockState.qml | 54 ++++++ modules/bar/states/Clock/qmldir | 1 + modules/bar/states/Network/NetworkState.qml | 188 +++++++++++++++++++- modules/bar/widgets/BarPopup.qml | 82 +++++++++ modules/bar/widgets/Clock.qml | 69 +++---- modules/bar/widgets/Network.qml | 183 +++++++++++++++++++ 7 files changed, 541 insertions(+), 37 deletions(-) create mode 100644 modules/bar/states/Clock/ClockState.qml create mode 100644 modules/bar/states/Clock/qmldir create mode 100644 modules/bar/widgets/BarPopup.qml diff --git a/config/Config.qml b/config/Config.qml index 50be077..fd1de90 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -14,6 +14,7 @@ QtObject { function font_size(spaceHeight) { return Math.floor(spaceHeight * 0.75) } function font_size_small(spaceHeight) { return Math.floor(spaceHeight * 0.625) } + function font_size_tiny(spaceHeight) { return Math.floor(spaceHeight * 0.4) } readonly property int small_radius: 8 readonly property int radius: 16 diff --git a/modules/bar/states/Clock/ClockState.qml b/modules/bar/states/Clock/ClockState.qml new file mode 100644 index 0000000..d7470d9 --- /dev/null +++ b/modules/bar/states/Clock/ClockState.qml @@ -0,0 +1,54 @@ +pragma Singleton +import QtQuick +import Quickshell +import Quickshell.Io + +QtObject { + id: root + + property string shortDate: Qt.formatDate(new Date(), "ddd, dd.MM.yyyy - ") + property string fullDate: Qt.formatDate(new Date(), "dddd, MMMM d, yyyy") + property string currentTime: Qt.formatTime(new Date(), "hh:mm:ss") + property string uptime: "Uptime: ..." + property bool expanded: false + + property Timer timeTimer: Timer { + interval: 1000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: root.currentTime = Qt.formatTime(new Date(), "hh:mm:ss") + } + + property Timer dateTimer: Timer { + interval: 60000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: { + root.shortDate = Qt.formatDate(new Date(), "ddd, dd.MM.yyyy - ") + root.fullDate = Qt.formatDate(new Date(), "dddd, MMMM d, yyyy") + } + } + + property Process sysInfoProcess: Process { + command: ["sh", "-c", "echo $(cat /proc/uptime)"] + stdout: StdioCollector { + onStreamFinished: { + var uptimeSecs = parseFloat(text.trim().split(" ")[0]) + var days = Math.floor(uptimeSecs / 86400) + var hours = Math.floor((uptimeSecs % 86400) / 3600) + var mins = Math.floor((uptimeSecs % 3600) / 60) + root.uptime = "Uptime: " + days + "d " + hours + "h " + mins + "m" + } + } + } + + property Timer sysInfoTimer: Timer { + interval: 10000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: root.sysInfoProcess.running = true + } +} diff --git a/modules/bar/states/Clock/qmldir b/modules/bar/states/Clock/qmldir new file mode 100644 index 0000000..ab24fd8 --- /dev/null +++ b/modules/bar/states/Clock/qmldir @@ -0,0 +1 @@ +singleton ClockState ClockState.qml diff --git a/modules/bar/states/Network/NetworkState.qml b/modules/bar/states/Network/NetworkState.qml index 7e2574c..2ec75dc 100644 --- a/modules/bar/states/Network/NetworkState.qml +++ b/modules/bar/states/Network/NetworkState.qml @@ -8,8 +8,32 @@ QtObject { id: root property string status: "..." + + property string interfaceName: "" + property string interfaceType: "" + property string connectionName: "" + property string ipv4: "" + property string signalStrength: "" + property string wifiChannel: "" + property string wifiFrequency: "" + property string linkSpeed: "" + property string downloadSpeed: "" + property string uploadSpeed: "" + property string ping: "" + property bool internetAvailable: false + property bool vpnConnected: false + property string vpnName: "" + property string vpnIp: "" + property bool tailscaleConnected: false + property string tailscaleIp: "" + property bool running: false + property string _activeDevice: "" + property real _prevRxBytes: 0 + property real _prevTxBytes: 0 + property int _prevUpdateTime: 0 + property Timer networkTimer: Timer { interval: 10000 repeat: true @@ -18,18 +42,172 @@ QtObject { } property Process networkScript: Process { - running: true - command: ["sh", "-c", Config.configDir + "/modules/bar/scripts/Network.sh"] + command: ["sh", "-c", + "echo '===SHOW==='; " + + "ACTIVE=$(nmcli -t device status 2>/dev/null | awk -F: '$3==\"connected\"{print $1; exit}'); " + + "echo \"ACTIVE_DEVICE=$ACTIVE\"; " + + "if [ -n \"$ACTIVE\" ]; then " + + " nmcli -t -f ALL device show \"$ACTIVE\" 2>/dev/null; " + + "fi; " + + "echo '===TRAFFIC==='; " + + "if [ -n \"$ACTIVE\" ]; then " + + " echo \"RX_BYTES=$(cat /sys/class/net/$ACTIVE/statistics/rx_bytes 2>/dev/null || echo 0)\"; " + + " echo \"TX_BYTES=$(cat /sys/class/net/$ACTIVE/statistics/tx_bytes 2>/dev/null || echo 0)\"; " + + "fi; " + + "echo '===WIFI==='; " + + "nmcli -t -f IN-USE,SSID,SIGNAL,CHAN,FREQ,RATE device wifi 2>/dev/null | grep '^\\\\*' | head -1 | sed 's/^\\*://'; " + + "echo '===PING==='; " + + "ping -c 1 -W 2 1.1.1.1 2>/dev/null | awk -F/ '/rtt/{print $5}' || echo ''; " + + "echo '===INTERNET==='; " + + "ping -c 1 -W 1 1.1.1.1 2>/dev/null >/dev/null && echo 1 || echo 0; " + + "echo '===VPN==='; " + + "nmcli -t -f NAME,TYPE connection show --active 2>/dev/null | grep -i vpn || true; " + + "echo '===TAILSCALE==='; " + + "TAIL_IP=$(ip -4 addr show tailscale0 2>/dev/null | grep -oP 'inet \\K[\\d.]+'); " + + "if [ -n \"$TAIL_IP\" ]; then echo \"TAILSCALE_IP=$TAIL_IP\"; else echo \"\"; fi" + ] stdout: StdioCollector { - onStreamFinished: { - root.status = this.text.trim() + onStreamFinished: root.parseNetworkOutput(text.trim()) + } + } + + function formatSpeed(bytesPerSec) { + if (bytesPerSec >= 1073741824) return (bytesPerSec / 1073741824).toFixed(1) + " GB/s" + if (bytesPerSec >= 1048576) return (bytesPerSec / 1048576).toFixed(1) + " MB/s" + if (bytesPerSec >= 1024) return (bytesPerSec / 1024).toFixed(1) + " KB/s" + return bytesPerSec.toFixed(0) + " B/s" + } + + function parseNetworkOutput(output) { + var sections = output.split("===TAILSCALE===") + var tailscaleSection = sections.length > 1 ? sections[1].trim() : "" + + var beforeTailscale = sections[0] + sections = beforeTailscale.split("===VPN===") + var vpnSection = sections.length > 1 ? sections[1].trim() : "" + + var beforeVpn = sections[0] + sections = beforeVpn.split("===INTERNET===") + root.internetAvailable = sections.length > 1 ? sections[1].trim() === "1" : false + + var beforeInternet = sections[0] + sections = beforeInternet.split("===PING===") + root.ping = sections.length > 1 ? sections[1].trim() : "" + + var beforePing = sections[0] + sections = beforePing.split("===WIFI===") + var wifiSection = sections.length > 1 ? sections[1].trim() : "" + + var beforeWifi = sections[0] + sections = beforeWifi.split("===TRAFFIC===") + var trafficSection = sections.length > 1 ? sections[1].trim() : "" + + var beforeTraffic = sections[0] + sections = beforeTraffic.split("===SHOW===") + var showSection = sections.length > 1 ? sections[1].trim() : "" + + // Parse SHOW section + var activeDevice = "" + var lines = showSection.split("\n") + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim() + if (line.indexOf("ACTIVE_DEVICE=") === 0) { + activeDevice = line.substring("ACTIVE_DEVICE=".length) + } else if (line.indexOf("GENERAL.DEVICE:") === 0) { + root.interfaceName = line.substring("GENERAL.DEVICE:".length) + } else if (line.indexOf("GENERAL.TYPE:") === 0) { + root.interfaceType = line.substring("GENERAL.TYPE:".length) + } else if (line.indexOf("GENERAL.CONNECTION:") === 0) { + root.connectionName = line.substring("GENERAL.CONNECTION:".length) + } else if (line.indexOf("IP4.ADDRESS") >= 0) { + var ipPart = line.substring(line.indexOf(":") + 1) + root.ipv4 = ipPart.split("/")[0] } } + + // Parse TRAFFIC section + if (trafficSection) { + var trafficLines = trafficSection.split("\n") + var rxBytes = 0 + var txBytes = 0 + for (var t = 0; t < trafficLines.length; t++) { + var tline = trafficLines[t].trim() + if (tline.indexOf("RX_BYTES=") === 0) rxBytes = parseFloat(tline.substring("RX_BYTES=".length)) + else if (tline.indexOf("TX_BYTES=") === 0) txBytes = parseFloat(tline.substring("TX_BYTES=".length)) + } + + if (activeDevice === root._activeDevice && root._prevRxBytes > 0 && root._prevUpdateTime > 0) { + var interval = (Date.now() - root._prevUpdateTime) / 1000 + if (interval > 0) { + root.downloadSpeed = root.formatSpeed((rxBytes - root._prevRxBytes) / interval) + root.uploadSpeed = root.formatSpeed((txBytes - root._prevTxBytes) / interval) + } + } else { + root.downloadSpeed = "" + root.uploadSpeed = "" + } + + root._activeDevice = activeDevice + root._prevRxBytes = rxBytes + root._prevTxBytes = txBytes + root._prevUpdateTime = Date.now() + } + + // Parse WIFI section + if (wifiSection) { + var wifiParts = wifiSection.split(":") + if (wifiParts.length >= 2) { + root.signalStrength = wifiParts[1] || "" + if (wifiParts.length >= 3) root.wifiChannel = wifiParts[2] || "" + if (wifiParts.length >= 4) root.wifiFrequency = wifiParts[3] || "" + if (wifiParts.length >= 5) root.linkSpeed = wifiParts[4] || "" + } + } + + // Determine status icon + if (root.interfaceType === "ethernet") { + root.status = "󰈀" + } else if (root.interfaceType === "wifi") { + if (!root.connectionName) { + root.status = "󰤭" + } else { + var sig = parseInt(root.signalStrength) + if (sig > 80) root.status = "󰤨" + else if (sig > 60) root.status = "󰤥" + else if (sig > 40) root.status = "󰤢" + else if (sig > 20) root.status = "󰤟" + else root.status = "󰤯" + } + } else if (!activeDevice) { + root.status = "󰤭/󱐤" + } + + // Parse VPN section + if (vpnSection) { + root.vpnConnected = true + var vpnLines = vpnSection.split("\n") + if (vpnLines.length > 0) { + var vpnParts = vpnLines[0].split(":") + root.vpnName = vpnParts[0] || "" + } + } else { + root.vpnConnected = false + root.vpnName = "" + root.vpnIp = "" + } + + // Parse TAILSCALE section + if (tailscaleSection && tailscaleSection.indexOf("TAILSCALE_IP=") === 0) { + root.tailscaleConnected = true + root.tailscaleIp = tailscaleSection.substring("TAILSCALE_IP=".length) + } else { + root.tailscaleConnected = false + root.tailscaleIp = "" + } } Component.onCompleted: { networkScript.running = true - networkTimer.start() } } \ No newline at end of file diff --git a/modules/bar/widgets/BarPopup.qml b/modules/bar/widgets/BarPopup.qml new file mode 100644 index 0000000..d4bf829 --- /dev/null +++ b/modules/bar/widgets/BarPopup.qml @@ -0,0 +1,82 @@ +import QtQuick +import Quickshell +import "../../../config" + +PopupWindow { + id: popup + + property Item anchorItem + property bool anchorHovered: false + property int popupWidth: 300 + property int popupVerticalOffset: 0 + + default property alias content: contentColumn.data + + visible: false + color: "transparent" + width: popupWidth + height: background.height + + onAnchorHoveredChanged: popup.updateVisibility() + + function updateVisibility() { + if (anchorHovered || mouseArea.containsMouse) { + closeTimer.stop() + visible = true + } else { + closeTimer.restart() + } + } + + Timer { + id: closeTimer + interval: 1 + onTriggered: { + if (!anchorHovered && !mouseArea.containsMouse) { + visible = false + } + } + } + + anchor { + item: anchorItem + edges: Edges.Top | Edges.Left + gravity: Edges.Bottom | Edges.Right + adjustment: PopupAdjustment.FlipY | PopupAdjustment.SlideX + onAnchoring: { + anchor.rect.x = (anchorItem.width - popup.width) / 2 + anchor.rect.y = anchorItem.height + popupVerticalOffset + } + } + + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onContainsMouseChanged: popup.updateVisibility() + Rectangle { + id: background + width: parent.width + height: contentColumn.implicitHeight + 2 * 12 + color: Qt.alpha(Colors.primaryContainer, 0.9) + radius: 12 + border { + color: Colors.primaryContainer_fg + width: Config.border_width + } + + Column { + id: contentColumn + height: implicitHeight + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: 12 + } + spacing: Config.small_spacing_fun(24) + } + } + } +} diff --git a/modules/bar/widgets/Clock.qml b/modules/bar/widgets/Clock.qml index ec54eb6..cd2e8b2 100644 --- a/modules/bar/widgets/Clock.qml +++ b/modules/bar/widgets/Clock.qml @@ -2,10 +2,11 @@ import QtQuick import Quickshell import Quickshell.Hyprland import "../../../config" +import "../states/Clock" Rectangle { //workspace id: clockRect - width: dateTimer.running ? Math.max(dateText.width + timeText.width + Config.spacing_fun(height)*3, bar.height) : Math.max(dateText.width + timeText.width + Config.spacing_fun(height)*2, bar.height) + width: ClockState.expanded ? Math.max(dateText.width + timeText.width + Config.spacing_fun(height)*3, bar.height) : Math.max(timeText.width + Config.spacing_fun(height)*2, bar.height) height: bar.height radius: Config.radius_fun(height) color: clockArea.containsMouse ? Colors.tertiaryContainer : Colors.primaryContainer @@ -22,9 +23,8 @@ Rectangle { //workspace anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton onClicked: { - dateTimer.running = !dateTimer.running - if (!dateTimer.running) dateText.currentDate = "" - console.log("[Clock] Date visible: " + dateTimer.running) + ClockState.expanded = !ClockState.expanded + console.log("[Clock] Date visible: " + ClockState.expanded) } Row { @@ -32,54 +32,59 @@ Rectangle { //workspace Text { id: dateText - property string currentDate: "" - text: currentDate + text: ClockState.shortDate + visible: ClockState.expanded color: ( clockArea.containsMouse ? Colors.tertiary : Colors.primary ) font.pixelSize: Config.font_size_small(clockRect.height) font.family: Config.font - - Timer { - id: dateTimer - interval: Config.reload_interval - running: false - repeat: true - triggeredOnStart: true - onTriggered: { - var now = new Date() - dateText.currentDate = Qt.formatDate(now, "ddd, dd.MM.yyyy - ") - } - } } Text { id: timeText property string currentTime: "" - text: currentTime + text: ClockState.currentTime color: ( clockArea.containsMouse ? Colors.tertiary : Colors.primary ) font.pixelSize: Config.font_size_small(clockRect.height) font.family: Config.font + } + } + } - Component.onCompleted: { - var now = new Date() - currentTime = Qt.formatTime(now, "hh:mm:ss") - } + BarPopup { + id: clockPopup + anchorItem: clockRect + anchorHovered: clockArea.containsMouse + width: Math.max(text1.width, text2.width) + (Config.spacing_fun(clockRect.height) * 3) + height: text1.height*3+(Config.spacing_fun(clockRect.height) * 5) - Timer { - interval: 1000 - running: true - repeat: true - onTriggered: { - var now = new Date() - timeText.currentTime = Qt.formatTime(now, "hh:mm:ss") - } - } + Column { + width: parent.width + spacing: Config.spacing_fun(clockRect.height) + Text { + id: text1 + text: ClockState.fullDate + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text2 + text: ClockState.uptime + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font } } } diff --git a/modules/bar/widgets/Network.qml b/modules/bar/widgets/Network.qml index 8254929..955cdfc 100644 --- a/modules/bar/widgets/Network.qml +++ b/modules/bar/widgets/Network.qml @@ -33,4 +33,187 @@ MouseArea { color: networkButton.containsMouse ? Colors.tertiary_fg : networkText.text === "󰤭 /󱐤" ? Colors.error : Colors.primary } } + + BarPopup { + id: clockPopup + anchorItem: networkButton + anchorHovered: networkButton.containsMouse + property int count: 17 + width: Math.max(text1.width, text2.width, text3.width, text4.width, text5.width, text6.width) + (Config.spacing_fun(networkButton.height) * 3) + height: text1.height*count+(Config.spacing_fun(networkButton.height) * (count + 2)) + + Column { + width: parent.width + spacing: Config.spacing_fun(networkButton.height) + Text { + id: text1 + text: "interfaceName: " + NetworkState.interfaceName + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text2 + text: "interfaceType: " + NetworkState.interfaceType + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text3 + text: "connectionName: " + NetworkState.connectionName + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text4 + text: "ipv4: " + NetworkState.ipv4 + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text5 + text: "signalStrength: " + NetworkState.signalStrength + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text6 + text: "wifiChannel: " + NetworkState.wifiChannel + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text7 + text: "wifiFrequency: " + NetworkState.wifiFrequency + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text8 + text: "linkSpeed: " + NetworkState.linkSpeed + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text9 + text: "downloadSpeed: " + NetworkState.downloadSpeed + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text10 + text: "uploadSpeed: " + NetworkState.uploadSpeed + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text11 + text: "ping: " + NetworkState.ping + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text12 + text: "internetAvailable: " + NetworkState.internetAvailable + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text13 + text: "vpnConnected: " + NetworkState.vpnConnected + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text14 + text: "vpnName: " + NetworkState.vpnName + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text15 + text: "vpnIp: " + NetworkState.vpnIp + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text16 + text: "tailscaleConnected: " + NetworkState.tailscaleConnected + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + + Text { + id: text17 + text: "tailscaleIp: " + NetworkState.tailscaleIp + wrapMode: Text.WordWrap + color: Colors.primary + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + } + } } \ No newline at end of file