# eww-bar — lifecycle manager for the eww daemon and the bar window. # # The daemon is owned by the user unit eww.service. Two eww facts shape this # script: # # * A client waits only 100 ms for the daemon's reply. Any slower command # (opening the popup, reload) fails on the client side, and a plain # `eww open` then forks a SECOND daemon that takes over the socket path and # orphans the first one's surfaces ("two bars", "a popup I can't close"). # Every client call here passes --no-daemonize, which turns that into a # harmless error; outcomes are verified with `active-windows`, never with # the exit status. # * `eww ping` is answered by the IPC thread; `eww active-windows` by the GTK # main loop. Only the latter proves the UI is alive. # # SCREEN is a GDK monitor index (0), a connector name (DP-1) or # desc:. Descriptions # survive re-plugging and profile changes; indices do not. usage() { cat <<'USAGE' usage: eww-bar COMMAND [ARGS] ensure make sure a healthy, service-owned daemon is running open [SCREEN] open the bar (no-op if already open there), remember SCREEN place [SCREEN] force re-open of the bar on SCREEN (used by kanshi) close close the bar and the popups reload reload the eww config and re-place the bar restart restart eww.service (the bar comes back with it) reset kill every eww process, wipe the sockets, start fresh check health check run by eww-watchdog.timer clean ExecStartPre: kill stray daemons, wipe stale sockets focused print the name of the focused Hyprland monitor status show unit state, socket owner, open windows and screen USAGE } uid=$(id -u) runtime_dir=${XDG_RUNTIME_DIR:-/run/user/$uid} state_dir=$runtime_dir/eww-bar lock_file=$state_dir/lock screen_file=$state_dir/screen # requested screen spec, re-resolved on restart current_file=$state_dir/current # resolved screen the bar is currently on fail_file=$state_dir/strikes unit=eww.service default_screen=0 max_strikes=2 mkdir -p "$state_dir" log() { printf 'eww-bar: %s\n' "$*" >&2; } # ---------------------------------------------------------------- client --- # fd 9 (the lock) is closed for every client call so nothing can inherit it. ewwc() { timeout 5 eww --no-daemonize "$@" 9>&-; } ping_daemon() { ewwc ping >/dev/null 2>&1; } list_windows() { ewwc active-windows 2>/dev/null || true; } bar_open() { list_windows | grep -qx 'bar: bar'; } # wait_for_window NAME [SECONDS] wait_for_window() { local i n=$((${2:-5} * 5)) for ((i = 0; i < n; i++)); do list_windows | grep -qx "$1: $1" && return 0 sleep 0.2 done return 1 } # ---------------------------------------------------------------- daemon --- # nixpkgs wraps eww (wrapGAppsHook), so the running process is `.eww-wrapped`; # `pkill -x eww` never matched it. Exact match, this user only. kill_stray_daemons() { pkill -TERM -x -u "$uid" '(\.eww-wrapped|eww)' 2>/dev/null || true sleep 0.3 pkill -KILL -x -u "$uid" '(\.eww-wrapped|eww)' 2>/dev/null || true rm -f "$runtime_dir"/eww-server_* } unit_state() { systemctl --user show -p ActiveState --value "$unit" 2>/dev/null || echo unknown; } unit_running() { case $(unit_state) in active | activating | reloading) return 0 ;; *) return 1 ;; esac } main_pid() { systemctl --user show -p MainPID --value "$unit" 2>/dev/null || echo 0; } # pids listening on an eww socket path. After a rival rebinds the path, ss # lists BOTH sockets under the same name (the old one is merely unlinked), so # every pid matters, not just the first. socket_owners() { ss -xlp 2>/dev/null | grep -F "$runtime_dir/eww-server_" \ | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | sort -u | tr '\n' ' ' } # The socket file is gone while the daemon lives on: a rival that crashed at # startup unlinked it, and no client can reach the daemon any more. socket_missing() { ! ls "$runtime_dir"/eww-server_* >/dev/null 2>&1; } daemon_age() { ps -o etimes= -p "$(main_pid)" 2>/dev/null | tr -d " " || echo 0; } # pids of daemons other than the service's main process listening on the socket rival_pids() { local main pid main=$(main_pid) for pid in $(socket_owners); do [[ $pid != "$main" ]] && printf '%s ' "$pid" done return 0 } rival_daemon() { [[ -n $(rival_pids) ]]; } healthy() { unit_running && ping_daemon && ! rival_daemon; } wait_for_daemon() { local i for ((i = 0; i < 100; i++)); do ping_daemon && return 0 sleep 0.1 done return 1 } start_unit() { systemctl --user reset-failed "$unit" >/dev/null 2>&1 || true systemctl --user start "$unit" } restart_unit() { log "restarting $unit" systemctl --user reset-failed "$unit" >/dev/null 2>&1 || true systemctl --user restart "$unit" } ensure() { healthy && return 0 ( flock -x 9 healthy && exit 0 if rival_daemon; then log "a second eww daemon (pid $(rival_pids)) owns the socket" elif unit_running && wait_for_daemon && ! rival_daemon; then # The unit was (re)starting — possibly running us from ExecStartPost. exit 0 fi log "daemon unhealthy, restarting $unit" systemctl --user stop "$unit" >/dev/null 2>&1 || true kill_stray_daemons start_unit wait_for_daemon ) 9>"$lock_file" } # --------------------------------------------------------------- screens --- focused_monitor() { hyprctl -j monitors 2>/dev/null | jq -r 'first(.[] | select(.focused) | .name) // empty' } # desc: → connector name via hyprctl; anything else is passed to eww. resolve_screen() { local spec=$1 name case $spec in desc:*) name=$(hyprctl -j monitors 2>/dev/null \ | jq -r --arg p "${spec#desc:}" '[.[] | select(.description | startswith($p))][0].name // empty') if [[ -n $name ]]; then printf '%s\n' "$name" else log "no monitor matches '$spec', using screen $default_screen" printf '%s\n' "$default_screen" fi ;; '') printf '%s\n' "$default_screen" ;; *) printf '%s\n' "$spec" ;; esac } saved_screen() { cat "$screen_file" 2>/dev/null || true; } current_screen() { cat "$current_file" 2>/dev/null || true; } # ------------------------------------------------------------------- bar --- close_popups() { local w for w in popup powermenu; do list_windows | grep -qx "$w: $w" && { ewwc close "$w" >/dev/null 2>&1 || true; } done ewwc update active-panel= >/dev/null 2>&1 || true } # open_bar [--force] [SPEC] open_bar() { local force=0 spec screen if [[ ${1:-} == --force ]]; then force=1; shift; fi spec=${1:-$(saved_screen)} spec=${spec:-$default_screen} ensure || { log "no daemon, cannot open the bar"; return 1; } screen=$(resolve_screen "$spec") ( flock -x 9 if ((force == 0)) && [[ $(current_screen) == "$screen" ]] && bar_open; then exit 0 fi # Popups may sit on an output that just vanished; `eww open` itself # replaces an already-open bar. The exit status is meaningless (100 ms # 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 fi printf '%s\n' "$spec" >"$screen_file" printf '%s\n' "$screen" >"$current_file" log "bar opened on screen $screen" ) 9>"$lock_file" } close_bar() { close_popups ewwc close bar >/dev/null 2>&1 || true rm -f "$current_file" } reload() { ensure || return 1 # Slow: the client always hits its deadline, so ignore the status and # verify that the daemon is still with us afterwards. ewwc reload >/dev/null 2>&1 || true sleep 1 if ! wait_for_daemon; then log "daemon lost during reload, restarting $unit" restart_unit return 0 fi open_bar --force } reset() { log "full reset" systemctl --user stop "$unit" >/dev/null 2>&1 || true kill_stray_daemons rm -f "$current_file" "$fail_file" start_unit # ExecStartPost re-opens the bar } # ExecStartPre clean() { kill_stray_daemons rm -f "$current_file" "$fail_file" } # -------------------------------------------------------------- watchdog --- # One bad check is tolerated (a busy daemon can miss eww's 100 ms IPC reply # deadline); the second consecutive one restarts the unit. strike() { local n n=$(($(cat "$fail_file" 2>/dev/null || echo 0) + 1)) if ((n >= max_strikes)); then log "$1 ($n/$max_strikes) — restarting $unit" rm -f "$fail_file" restart_unit else log "$1 ($n/$max_strikes)" printf '%s\n' "$n" >"$fail_file" fi } check() { systemctl --user is-active --quiet graphical-session.target || return 0 if ! unit_running; then log "$unit is not running, starting it" start_unit || true return 0 fi [[ $(unit_state) == active ]] || return 0 # still starting; ExecStartPost handles it if rival_daemon; then log "a second eww daemon (pid $(rival_pids)) owns the socket, restarting $unit" restart_unit return 0 fi if socket_missing && (($(daemon_age) > 15)); then log "socket file is gone, restarting $unit" restart_unit return 0 fi if ! ping_daemon; then 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. open_bar || true if wait_for_window bar 3; then rm -f "$fail_file" log "bar was missing, reopened" else strike "UI unresponsive" fi } status() { systemctl --user --no-pager status "$unit" 2>&1 | head -n 12 || true printf '\nsocket listeners: %s(service main pid %s)\n' "$(socket_owners)" "$(main_pid)" printf 'windows:\n' list_windows | sed 's/^/ /' printf 'screen: %s (requested: %s)\n' "$(current_screen)" "$(saved_screen)" printf 'strikes: %s\n' "$(cat "$fail_file" 2>/dev/null || echo 0)" } case ${1:-} in ensure) ensure ;; open) shift; open_bar "$@" ;; place) shift; open_bar --force "$@" ;; close) close_bar ;; reload) reload ;; restart) restart_unit ;; reset) reset ;; check) check ;; clean) clean ;; focused) focused_monitor ;; status) status ;; -h | --help | help) usage ;; *) usage >&2; exit 64 ;; esac