# eww-bar — lifecycle manager for the eww daemon and the bar window. # # The daemon is owned by the user unit eww.service. Every window open goes # through `ensure` first: a plain `eww open` against a dead or stale socket # silently forks a *second* daemon that takes over the socket path and orphans # the first daemon's surfaces ("a bar/popup I can't close"). # # 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, open windows and the bar's 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; } # ---------------------------------------------------------------- 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 } # `eww ping` is answered by the IPC thread and only proves the socket is alive. ping_daemon() { timeout 3 eww ping >/dev/null 2>&1 9>&-; } # `active-windows` is answered from the GTK main loop, so it also detects a # frozen UI: the reply is then empty instead of listing the windows. # fd 9 is closed for every client call so a daemonizing client (which can # only happen when the socket is dead) can never inherit our lock. list_windows() { timeout 5 eww active-windows 2>/dev/null 9>&- || true; } bar_open() { list_windows | grep -qx 'bar: bar'; } healthy() { unit_running && ping_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 # The unit is (re)starting — possibly running us from ExecStartPost — # so give the daemon time to bind before declaring it dead. if unit_running && wait_for_daemon; then exit 0; fi log "daemon unreachable, 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 timeout 3 eww close "$w" >/dev/null 2>&1 9>&- || true done timeout 3 eww update active-panel= >/dev/null 2>&1 9>&- || 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. close_popups if ! timeout 10 eww open bar --screen "$screen" >/dev/null 9>&-; then if [[ $screen == "$default_screen" ]]; then log "eww open failed, restarting $unit" restart_unit exit 1 fi log "cannot open the bar on '$screen', using screen $default_screen" screen=$default_screen timeout 10 eww open bar --screen "$screen" >/dev/null 9>&- || { restart_unit; exit 1; } 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 timeout 3 eww close bar >/dev/null 2>&1 9>&- || true rm -f "$current_file" } reload() { ensure || return 1 if ! timeout 15 eww reload >/dev/null 9>&-; then log "reload failed, restarting $unit" restart_unit return 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 ! ping_daemon; then strike "daemon does not answer" return 0 fi if bar_open; 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 bar_open; 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 '\nwindows:\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