371 lines
12 KiB
Bash
371 lines
12 KiB
Bash
# 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:<prefix of the description shown by `hyprctl monitors`>. 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; }
|
|
# 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'; }
|
|
|
|
# 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
|
|
}
|
|
|
|
# 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`;
|
|
# `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 --no-block "$unit"
|
|
}
|
|
|
|
restart_unit() {
|
|
log "restarting $unit"
|
|
systemctl --user reset-failed "$unit" >/dev/null 2>&1 || true
|
|
systemctl --user restart --no-block "$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:<prefix> → 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 state
|
|
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
|
|
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"
|
|
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
|
|
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 5; 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
|