vpn eww and eww crash

This commit is contained in:
2026-09-10 21:02:27 +02:00
parent 7bb27f9373
commit a53f120b63
14 changed files with 246 additions and 51 deletions
@@ -34,3 +34,10 @@
.bt-device-btn:hover { @include background-active; }
.net-toggle-on, .bt-btn-on { @include color-base; }
.net-toggle-off, .bt-btn-off { @include color-inactive; }
// WireGuard
.wg-accent { background-color: $base0E; }
.wg-row { margin-bottom: $popup-scale * 4pt; }
.wg-name { font-size: 0.78em; font-weight: bold; }
.wg-up { @include color-base; }
.wg-down { @include color-inactive; }
@@ -4,8 +4,8 @@
(defwidget clock-mod []
(module
(eventbox
:onhover "${EWW_CMD} update date_rev=true"
:onhoverlost "${EWW_CMD} update date_rev=false"
:onhover "${EWW_CMD} --no-daemonize update date_rev=true"
:onhoverlost "${EWW_CMD} --no-daemonize update date_rev=false"
:onclick "scripts/panel-toggle clock"
:onrightclick "scripts/powermenu-toggle"
(box
@@ -1,5 +1,5 @@
(deflisten net
:initial '{"wifi":{"connected":false,"enabled":false,"icon":"󰤮","ssid":"","ip":"","freq":0,"band":"","gen":"","signal":0},"ethernet":{"connected":false,"interface":"","ip":"","speed":""},"usb":{"connected":false,"interface":"","ip":""}}'
:initial '{"wifi":{"connected":false,"enabled":false,"icon":"󰤮","ssid":"","ip":"","freq":0,"band":"","gen":"","signal":0},"ethernet":{"connected":false,"interface":"","ip":"","speed":""},"usb":{"connected":false,"interface":"","ip":""},"wireguard":[],"vpn":{"up":false,"name":""}}'
"scripts/net/net")
(deflisten bt
@@ -20,6 +20,11 @@
:class "net-icon net-active"
:tooltip "USB: Connected"
:text "󰌷")
(label
:visible {net.vpn.up}
:class "net-icon net-active"
:tooltip "VPN: ${net.vpn.name}"
:text "󰖂")
(label
:class "net-icon ${net.wifi.connected ? 'net-active' : 'net-dim'}"
:tooltip {net.wifi.connected ? "WiFi: ${net.wifi.ssid}" : "WiFi: Disconnected"}
@@ -3,6 +3,13 @@
# top of every wrapped script, right after setting PATH.
# ---------------------------------------------------------------------------
# Every eww client call goes through this wrapper. A client waits only 100 ms
# for the daemon's reply; a slower command (opening the popup) would otherwise
# make a plain `eww open` fork a second daemon that takes over the socket —
# the "two bars" bug. With --no-daemonize it is just an error, and the daemon
# still carries out the command.
eww() { command eww --no-daemonize "$@"; }
# Print the payload only when it differs from the previous one.
emit() {
[[ $1 == "${EMIT_LAST-}" ]] && return 0
@@ -70,8 +70,60 @@ state() {
"$w_connected" "$w_enabled" "$w_icon" "$(jstr "$w_ssid")" "$w_ip" "${w_freq:-0}" "$(band "$w_freq")" "$w_gen" "${w_signal:-0}"
printf '"ethernet":{"connected":%s,"interface":"%s","ip":"%s","speed":"%s"},' \
"$e_connected" "$eth" "$e_ip" "$e_speed"
printf '"usb":{"connected":%s,"interface":"%s","ip":"%s"}}' \
printf '"usb":{"connected":%s,"interface":"%s","ip":"%s"},' \
"$u_connected" "$usb" "$u_ip"
wg_json
printf '}'
}
# --- WireGuard: interfaces from the wireguard-<name>.service units (so a
# tunnel that is down still shows up) plus any live wireguard-type link.
# `wg show` needs root, so peer names and the endpoint come from the peer
# units instead.
wg_ifaces() {
{
systemctl list-units --all --plain --no-legend 'wireguard-*.service' 2>/dev/null \
| awk '$1 !~ /-peer-/ { sub(/^wireguard-/, "", $1); sub(/\.service$/, "", $1); print $1 }'
ip -d -o link 2>/dev/null | awk '/ wireguard / { sub(":", "", $2); print $2 }'
} | sort -u
}
human_bytes() {
awk -v b="${1:-0}" 'BEGIN {
split("B KiB MiB GiB TiB", u, " "); i = 1
while (b >= 1024 && i < 5) { b /= 1024; i++ }
printf (i == 1 ? "%d %s" : "%.1f %s"), b, u[i] }'
}
wg_json() { # prints the "wireguard" array and the "vpn" summary
local name up ip rx tx peers endpoint peer_unit peer_script sep="" out="[" vpn_up=false vpn_name=""
while read -r name; do
[[ -n $name ]] || continue
up=false ip="" rx=0 tx=0 endpoint=""
if [[ -d /sys/class/net/$name ]]; then
ip -o link show "$name" 2>/dev/null | grep -q '<[^>]*UP' && up=true
ip=$(ipv4 "$name")
rx=$(<"/sys/class/net/$name/statistics/rx_bytes")
tx=$(<"/sys/class/net/$name/statistics/tx_bytes")
fi
peers=$(systemctl list-units --all --plain --no-legend "wireguard-$name-peer-*.service" 2>/dev/null \
| awk '{ print $1 }' | sed "s/^wireguard-$name-peer-//; s/\.service\$//" | paste -sd, -)
peer_unit=$(systemctl list-units --all --plain --no-legend "wireguard-$name-peer-*.service" 2>/dev/null \
| awk 'NR == 1 { print $1 }')
# NixOS puts `wg set … endpoint "host:port"` in the peer unit's start script
if [[ -n $peer_unit ]]; then
peer_script=$(systemctl show -p ExecStart --value "$peer_unit" 2>/dev/null \
| sed -n 's/.*path=\([^ ;]*\).*/\1/p' | awk 'NR == 1')
[[ -r $peer_script ]] && endpoint=$(grep -o 'endpoint "[^"]*"' "$peer_script" 2>/dev/null \
| awk -F'"' 'NR == 1 { print $2 }')
fi
if [[ $up == true && $vpn_up == false ]]; then vpn_up=true vpn_name=$name; fi
[[ -z $vpn_name ]] && vpn_name=$name
out+="$sep{\"name\":\"$(jstr "$name")\",\"up\":$up,\"ip\":\"$ip\",\"rx\":\"$(human_bytes "$rx")\",\"tx\":\"$(human_bytes "$tx")\",\"peers\":\"$(jstr "$peers")\",\"endpoint\":\"$(jstr "$endpoint")\"}"
sep=,
done < <(wg_ifaces)
printf '"wireguard":%s],"vpn":{"up":%s,"name":"%s"}' "$out" "$vpn_up" "$(jstr "$vpn_name")"
}
refresh() { emit "$(state)"; }
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Bring the WireGuard tunnel $1 down (stop its service, which takes the peer
# units with it) or up (start its target, which pulls service and peers).
# Needs the polkit rule from modules/nixos/system/network/wireguard.
name=$1
[[ -n $name ]] || exit 64
if [[ -d /sys/class/net/$name ]]; then
systemctl stop "wireguard-$name.service"
else
systemctl start "wireguard-$name.target"
fi
@@ -4,6 +4,11 @@
# The popup hosts every panel behind revealers, so switching panels while it
# is open is just a variable update (animated, no scripts restarted). It is
# only closed and re-opened when it has to move to another monitor.
#
# `eww open popup` takes longer than the client's 100 ms reply deadline, so
# its exit status and error output are meaningless and ignored (the daemon
# carries the command out anyway; the lib's eww wrapper makes sure a slow
# reply can never spawn a second daemon).
panel=$1
[[ -n $panel ]] || exit 64
@@ -12,7 +17,7 @@ panel=$1
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/eww-panel-toggle.lock"
flock -n 9 || exit 0
ew() { timeout 5 eww "$@" 9>&-; }
ew() { eww "$@" 9>&-; }
current=$(ew get active-panel 2>/dev/null | tr -d '"')
popup_screen=$(ew get popup-screen 2>/dev/null | tr -d '"')
@@ -21,15 +26,16 @@ if ew active-windows 2>/dev/null | grep -qx 'popup: popup'; then open=1; else op
if ((open)) && [[ $current == "$panel" ]]; then
ew update active-panel=
ew close popup
ew close popup 2>/dev/null
elif ((open)) && [[ -n $screen && $popup_screen == "$screen" ]]; then
ew update active-panel="$panel"
else
((open)) && ew close popup
((open)) && ew close popup 2>/dev/null
ew update active-panel="$panel" popup-screen="$screen"
if [[ -n $screen ]]; then
ew open popup --screen "$screen"
ew open popup --screen "$screen" >/dev/null 2>&1
else
ew open popup
ew open popup >/dev/null 2>&1
fi
fi
exit 0
@@ -1,11 +1,12 @@
#!/usr/bin/env bash
# Toggle the full-screen power menu on the focused monitor.
ew() { timeout 5 eww "$@"; }
ew close popup 2>/dev/null
ew update active-panel=
# Toggle the full-screen power menu on the focused monitor. The open's exit
# status is ignored: it exceeds the client's 100 ms reply deadline.
eww close popup 2>/dev/null
eww update active-panel= 2>/dev/null
screen=$(eww-bar focused)
if [[ -n $screen ]]; then
ew open powermenu --toggle --screen "$screen"
eww open powermenu --toggle --screen "$screen" >/dev/null 2>&1
else
ew open powermenu --toggle
eww open powermenu --toggle >/dev/null 2>&1
fi
exit 0
@@ -83,6 +83,29 @@
:visible {!bt.powered}
:text "Disabled")))
; --- WireGuard ---
(defwidget wg-row [wg]
(box :orientation "v" :space-evenly false :class "wg-row"
(box :orientation "h" :space-evenly false :valign "center"
(label :class "wg-name ${wg.up ? 'wg-up' : 'wg-down'}" :halign "start" :hexpand true :text {wg.name})
(button
:class "net-toggle-btn ${wg.up ? 'net-toggle-on' : 'net-toggle-off'}"
:onclick "scripts/net/wg-toggle ${wg.name}"
:tooltip {wg.up ? "Disconnect" : "Connect"}
(label :text {wg.up ? "󰖂" : "󰦞"})))
(box :orientation "v" :space-evenly false :visible {wg.up}
(netinfo-row :label "IP" :value {wg.ip})
(netinfo-row :label "Peer" :value {wg.endpoint != "" ? "${wg.peers} · ${wg.endpoint}" : wg.peers})
(netinfo-row :label "Traffic" :value "󰇚 ${wg.rx} 󰕒 ${wg.tx}"))
(label :class "netinfo-dim" :halign "start" :visible {!wg.up} :text "Down")))
(defwidget wireguard-net-section []
(box :orientation "v" :space-evenly false :class "sys-section"
(section-header :title "WireGuard" :accent "wg-accent")
(for wg in {net.wireguard}
(wg-row :wg {wg}))))
; --- Root ---
(defwidget net-win []
@@ -94,4 +117,7 @@
(box :class "section-sep")
(wifi-net-section)
(box :class "section-sep")
(box :visible {arraylength(net.wireguard) > 0} :space-evenly false :orientation "v"
(wireguard-net-section)
(box :class "section-sep"))
(bluetooth-net-section)))
@@ -22,7 +22,7 @@
(pm-btn :icon "󰗼" :label "Sign out"
:onclick "hyprctl eval \"hl.dispatch(hl.dsp.exit())\"")
(pm-btn :icon "󰅖" :label "Cancel"
:onclick "eww close powermenu")))))))
:onclick "${EWW_CMD} --no-daemonize close powermenu")))))))
(defwindow powermenu
:monitor 0
@@ -20,6 +20,7 @@ let
"scripts/net/net" = mkScript "net" ./bar/scripts/net/net [ iproute2 wpa_supplicant ];
"scripts/net/bt" = mkScript "bt" ./bar/scripts/net/bt [ jq glib ];
"scripts/net/bt-toggle" = mkScript "bt-toggle" ./bar/scripts/net/bt-toggle [ bluez ];
"scripts/net/wg-toggle" = mkScript "wg-toggle" ./bar/scripts/net/wg-toggle [ ];
"scripts/sys/gpu" = mkScript "gpu" ./bar/scripts/sys/gpu [ custom.amdgpu_top jq ];
"scripts/sys/memory" = mkScript "memory" ./bar/scripts/sys/memory [ ];
"scripts/sys/battery" = mkScript "battery" ./bar/scripts/sys/battery [ ];