From c8251c6f6dc5c3f20c66811e735d563a76c274b2 Mon Sep 17 00:00:00 2001 From: soraefir Date: Fri, 11 Sep 2026 22:40:11 +0200 Subject: [PATCH] fix freeze eww --- .../home/wayland/apps/eww/bar/scripts/_lib.sh | 12 ++++ .../apps/eww/bar/scripts/airplane-mode | 1 + .../apps/eww/bar/scripts/do-not-disturb | 1 + .../apps/eww/bar/scripts/net/bt-toggle | 2 + .../apps/eww/bar/scripts/net/wg-toggle | 1 + .../wayland/apps/eww/bar/scripts/nightlight | 1 + .../wayland/apps/eww/bar/scripts/panel-toggle | 1 + .../wayland/apps/eww/bar/scripts/power-save | 1 + .../apps/eww/bar/scripts/powermenu-toggle | 1 + .../home/wayland/apps/eww/bar/scripts/radio | 10 +-- .../wayland/apps/eww/bar/scripts/read-mode | 1 + packages/eww-bar/eww-bar.sh | 70 +++++++++++++------ 12 files changed, 76 insertions(+), 26 deletions(-) diff --git a/modules/home/wayland/apps/eww/bar/scripts/_lib.sh b/modules/home/wayland/apps/eww/bar/scripts/_lib.sh index 4086653..76264a5 100644 --- a/modules/home/wayland/apps/eww/bar/scripts/_lib.sh +++ b/modules/home/wayland/apps/eww/bar/scripts/_lib.sh @@ -39,6 +39,18 @@ detach() { systemd-run --user --collect --quiet "${unit[@]}" -- "$cmd" "$@" } +# eww kills a click handler that has not returned within the widget's +# :timeout (200 ms) — less than the eww client calls most handlers make +# (~50 ms each), let alone bluetoothctl or a station list fetch. Re-run +# this script detached and return at once. Call it with the original +# arguments, before anything with side effects. +# detach_self "$@" +detach_self() { + [[ -n ${EWW_DETACHED-} ]] && return 0 + EWW_DETACHED=1 setsid -f "$0" "$@" + exit 0 +} + # Event multiplexer for listener scripts. Producers run in the background # and write one line per event into a fifo; the main loop re-evaluates the # state on every event, coalescing bursts into a single refresh. diff --git a/modules/home/wayland/apps/eww/bar/scripts/airplane-mode b/modules/home/wayland/apps/eww/bar/scripts/airplane-mode index 4b33f62..080c1d2 100644 --- a/modules/home/wayland/apps/eww/bar/scripts/airplane-mode +++ b/modules/home/wayland/apps/eww/bar/scripts/airplane-mode @@ -5,6 +5,7 @@ active() { rfkill list wifi 2>/dev/null | grep -q "Soft blocked: yes"; } case ${1:-} in status) active && echo true || echo false ;; *) + detach_self "$@" if active; then rfkill unblock all state=false diff --git a/modules/home/wayland/apps/eww/bar/scripts/do-not-disturb b/modules/home/wayland/apps/eww/bar/scripts/do-not-disturb index 8f5cf98..a403e61 100644 --- a/modules/home/wayland/apps/eww/bar/scripts/do-not-disturb +++ b/modules/home/wayland/apps/eww/bar/scripts/do-not-disturb @@ -3,6 +3,7 @@ case ${1:-} in status) dunstctl is-paused ;; *) + detach_self "$@" dunstctl set-paused toggle state=$(dunstctl is-paused) eww update do-not-disturb="$state" 2>/dev/null diff --git a/modules/home/wayland/apps/eww/bar/scripts/net/bt-toggle b/modules/home/wayland/apps/eww/bar/scripts/net/bt-toggle index bb08919..0f2ded3 100755 --- a/modules/home/wayland/apps/eww/bar/scripts/net/bt-toggle +++ b/modules/home/wayland/apps/eww/bar/scripts/net/bt-toggle @@ -1,6 +1,8 @@ #!/usr/bin/env bash # Connect or disconnect the bluetooth device with MAC $1. mac=$1 +[[ -n $mac ]] || exit 64 +detach_self "$@" if bluetoothctl info "$mac" 2>/dev/null | grep -q "Connected: yes"; then bluetoothctl disconnect "$mac" else diff --git a/modules/home/wayland/apps/eww/bar/scripts/net/wg-toggle b/modules/home/wayland/apps/eww/bar/scripts/net/wg-toggle index 13c1a4e..59ae33e 100644 --- a/modules/home/wayland/apps/eww/bar/scripts/net/wg-toggle +++ b/modules/home/wayland/apps/eww/bar/scripts/net/wg-toggle @@ -4,6 +4,7 @@ # Needs the polkit rule from modules/nixos/system/network/wireguard. name=$1 [[ -n $name ]] || exit 64 +detach_self "$@" if [[ -d /sys/class/net/$name ]]; then systemctl stop "wireguard-$name.service" else diff --git a/modules/home/wayland/apps/eww/bar/scripts/nightlight b/modules/home/wayland/apps/eww/bar/scripts/nightlight index 8d47b0a..48a8516 100755 --- a/modules/home/wayland/apps/eww/bar/scripts/nightlight +++ b/modules/home/wayland/apps/eww/bar/scripts/nightlight @@ -8,6 +8,7 @@ active() { systemctl --user is-active --quiet "$unit" || pgrep -x wlsunset >/dev case ${1:-} in status) active && echo true || echo false ;; *) + detach_self "$@" if active; then systemctl --user stop "$unit" 2>/dev/null pkill -x wlsunset diff --git a/modules/home/wayland/apps/eww/bar/scripts/panel-toggle b/modules/home/wayland/apps/eww/bar/scripts/panel-toggle index f1d2819..1decc5f 100755 --- a/modules/home/wayland/apps/eww/bar/scripts/panel-toggle +++ b/modules/home/wayland/apps/eww/bar/scripts/panel-toggle @@ -11,6 +11,7 @@ # reply can never spawn a second daemon). panel=$1 [[ -n $panel ]] || exit 64 +detach_self "$@" # One toggle at a time; the lock dies with this process. fd 9 is closed for # every eww call so nothing can inherit it. diff --git a/modules/home/wayland/apps/eww/bar/scripts/power-save b/modules/home/wayland/apps/eww/bar/scripts/power-save index 5392cb6..bb0a9cf 100755 --- a/modules/home/wayland/apps/eww/bar/scripts/power-save +++ b/modules/home/wayland/apps/eww/bar/scripts/power-save @@ -5,6 +5,7 @@ active() { [[ $(powerprofilesctl get 2>/dev/null) == power-saver ]]; } case ${1:-} in status) active && echo true || echo false ;; *) + detach_self "$@" if active; then powerprofilesctl set balanced 2>/dev/null state=false diff --git a/modules/home/wayland/apps/eww/bar/scripts/powermenu-toggle b/modules/home/wayland/apps/eww/bar/scripts/powermenu-toggle index 1a6b9b0..3274c90 100755 --- a/modules/home/wayland/apps/eww/bar/scripts/powermenu-toggle +++ b/modules/home/wayland/apps/eww/bar/scripts/powermenu-toggle @@ -1,6 +1,7 @@ #!/usr/bin/env bash # 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. +detach_self "$@" eww close popup 2>/dev/null eww update active-panel= 2>/dev/null screen=$(eww-bar focused) diff --git a/modules/home/wayland/apps/eww/bar/scripts/radio b/modules/home/wayland/apps/eww/bar/scripts/radio index 1cd0e8b..e44d9d8 100755 --- a/modules/home/wayland/apps/eww/bar/scripts/radio +++ b/modules/home/wayland/apps/eww/bar/scripts/radio @@ -216,10 +216,10 @@ status_loop() { } case ${1:-} in - start) start "$2" ;; - stop) stop ;; - toggle) if playing; then stop; else start "$(current_id)"; fi ;; - vol) set_volume "$2" ;; - mute) toggle_mute ;; + start) detach_self "$@"; start "$2" ;; + stop) detach_self "$@"; stop ;; + toggle) detach_self "$@"; if playing; then stop; else start "$(current_id)"; fi ;; + vol) detach_self "$@"; set_volume "$2" ;; + mute) detach_self "$@"; toggle_mute ;; *) status_loop ;; esac diff --git a/modules/home/wayland/apps/eww/bar/scripts/read-mode b/modules/home/wayland/apps/eww/bar/scripts/read-mode index e30c521..61b38e1 100644 --- a/modules/home/wayland/apps/eww/bar/scripts/read-mode +++ b/modules/home/wayland/apps/eww/bar/scripts/read-mode @@ -8,6 +8,7 @@ active() { hyprctl getoption decoration:screen_shader 2>/dev/null | grep -qF "$s case ${1:-} in status) active && echo true || echo false ;; *) + detach_self "$@" if active; then hyprctl eval 'hl.config({ decoration = { screen_shader = "" } })' >/dev/null hyprctl eval 'hl.config({ render = { use_fp16 = 2 } })' >/dev/null diff --git a/packages/eww-bar/eww-bar.sh b/packages/eww-bar/eww-bar.sh index 2116819..af2f265 100644 --- a/packages/eww-bar/eww-bar.sh +++ b/packages/eww-bar/eww-bar.sh @@ -56,6 +56,9 @@ log() { printf 'eww-bar: %s\n' "$*" >&2; } ewwc() { timeout 5 eww --no-daemonize "$@" 9>&-; } ping_daemon() { ewwc ping >/dev/null 2>&1; } +# Empty when the daemon did not answer within the client's 100 ms deadline — +# its main thread is busy (a popup takes seconds to build) — as well as when +# nothing is open. Only a non-empty listing is authoritative. list_windows() { ewwc active-windows 2>/dev/null || true; } bar_open() { list_windows | grep -qx 'bar: bar'; } @@ -69,6 +72,28 @@ wait_for_window() { return 1 } +# window_state NAME [SECONDS] → open | closed (listed without NAME) | +# busy (nothing listed within SECONDS: main thread stuck, or nothing open) +window_state() { + local i n=$((${2:-5} * 5)) out + for ((i = 0; i < n; i++)); do + out=$(list_windows) + if [[ -n $out ]]; then + if grep -qx "$1: $1" <<<"$out"; then echo open; else echo closed; fi + return 0 + fi + sleep 0.2 + done + echo busy +} + +# The compositor's view: does the daemon own any layer surface? Tells a +# daemon that is merely not answering from one with nothing on screen. +daemon_has_layer() { + hyprctl -j layers 2>/dev/null \ + | jq -e --argjson pid "$(main_pid)" '[.. | objects | select(.pid? == $pid)] | length > 0' >/dev/null 2>&1 +} + # ---------------------------------------------------------------- daemon --- # nixpkgs wraps eww (wrapGAppsHook), so the running process is `.eww-wrapped`; @@ -126,13 +151,13 @@ wait_for_daemon() { start_unit() { systemctl --user reset-failed "$unit" >/dev/null 2>&1 || true - systemctl --user start "$unit" + systemctl --user start --no-block "$unit" } restart_unit() { log "restarting $unit" systemctl --user reset-failed "$unit" >/dev/null 2>&1 || true - systemctl --user restart "$unit" + systemctl --user restart --no-block "$unit" } ensure() { @@ -194,7 +219,7 @@ close_popups() { # open_bar [--force] [SPEC] open_bar() { - local force=0 spec screen + local force=0 spec screen state if [[ ${1:-} == --force ]]; then force=1; shift; fi spec=${1:-$(saved_screen)} spec=${spec:-$default_screen} @@ -210,17 +235,18 @@ open_bar() { # client deadline), so the result is verified instead. close_popups ewwc open bar --screen "$screen" >/dev/null 2>&1 || true - if ! wait_for_window bar 5; then - if [[ $screen != "$default_screen" ]]; then - log "cannot open the bar on '$screen', using screen $default_screen" - screen=$default_screen - ewwc open bar --screen "$screen" >/dev/null 2>&1 || true - fi - if ! wait_for_window bar 5; then - log "the bar does not come up, restarting $unit" - restart_unit - exit 1 - fi + state=$(window_state bar 10) + # Only an answered "no bar" means the screen was refused; busy just needs time. + if [[ $state == closed && $screen != "$default_screen" ]]; then + log "cannot open the bar on '$screen', using screen $default_screen" + screen=$default_screen + ewwc open bar --screen "$screen" >/dev/null 2>&1 || true + state=$(window_state bar 10) + fi + if [[ $state != open ]]; then + log "the bar does not come up ($state), restarting $unit" + restart_unit + exit 1 fi printf '%s\n' "$spec" >"$screen_file" printf '%s\n' "$screen" >"$current_file" @@ -301,14 +327,16 @@ check() { strike "daemon does not answer" return 0 fi - if wait_for_window bar 3; then - rm -f "$fail_file" - return 0 - fi - # Either nothing is open or the main loop is frozen (empty reply). Try to - # open the bar: a frozen daemon still won't list it afterwards. + case $(window_state bar 10) in + open) rm -f "$fail_file"; return 0 ;; + # Nothing listed for 10 s while something is on screen: the main loop is + # frozen or still building a popup. Leave the windows alone; a second + # miss restarts the unit. + busy) if daemon_has_layer; then strike "UI unresponsive"; return 0; fi ;; + esac + # Listed without the bar, or nothing on screen at all: it really is gone. open_bar || true - if wait_for_window bar 3; then + if wait_for_window bar 5; then rm -f "$fail_file" log "bar was missing, reopened" else