#!/bin/bash # Pre-flight check for everything this quickshell config depends on outside # its own QML — fonts, the compositor, backing D-Bus services, and the # external CLIs individual bar modules shell out to. Run this after # installing/stowing quickshell on a new machine, before assuming the bar # is just going to work. # # Usage: ./CheckQuickshellSetup.sh [path-to-quickshell-config] # Defaults to $XDG_CONFIG_HOME/quickshell or ~/.config/quickshell. set -uo pipefail QS_DIR="${1:-${XDG_CONFIG_HOME:-$HOME/.config}/quickshell}" PASS=0 WARN=0 FAIL=0 c_green="\033[0;32m"; c_yellow="\033[0;33m"; c_red="\033[0;31m"; c_reset="\033[0m" pass() { printf " ${c_green}✓${c_reset} %s\n" "$1"; PASS=$((PASS+1)); } warn() { printf " ${c_yellow}!${c_reset} %s\n" "$1"; WARN=$((WARN+1)); } fail() { printf " ${c_red}✗${c_reset} %s\n" "$1"; FAIL=$((FAIL+1)); } section() { printf "\n%s\n" "$1"; } svc_state() { # active / inactive / missing / no-systemctl. $1 = unit, $2 = --user (optional) # NB: capture-then-here-string, not `cmd | grep -q`. Under `set -o # pipefail`, grep -q exits the instant it finds a match, SIGPIPE-ing a # still-writing producer — that producer's non-zero exit (not grep's # success) becomes the pipeline's status, which reads as "not found" # even when it was. Verified this actually happens with fc-list below. if ! command -v systemctl >/dev/null 2>&1; then echo "no-systemctl"; return; fi local unit_files unit_files=$(systemctl ${2:-} list-unit-files "$1" 2>/dev/null) if grep -q "$1" <<< "$unit_files"; then systemctl ${2:-} is-active "$1" 2>/dev/null || echo "inactive" else echo "missing" fi } # --- 0. Quickshell itself + config files present ---------------------------- section "Quickshell core" if command -v quickshell >/dev/null 2>&1; then ver=$(quickshell --version 2>&1 | head -1) pass "quickshell found ($(command -v quickshell)) — $ver" else fail "quickshell binary not found on PATH — install it before anything else here matters" fi for f in "shell.qml" "config/Appearance.qml" "config/Colors.qml" "config/qmldir"; do if [ -f "$QS_DIR/$f" ]; then pass "found $f" else fail "missing $QS_DIR/$f — dotfiles not fully stowed/symlinked here?" fi done # Nearly every module (ActiveWindow, Workspaces, PowerButton, PanelWindow's # layer-shell surfaces, HyprlandFocusGrab) assumes a live Hyprland session. if [ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ] && command -v hyprctl >/dev/null 2>&1 && hyprctl monitors >/dev/null 2>&1; then pass "Hyprland is the running compositor" else fail "Hyprland doesn't appear to be running — Quickshell.Hyprland and the layer-shell bar will not function" fi # --- 1. Fonts ----------------------------------------------------------------- section "Fonts" if command -v fc-list >/dev/null 2>&1; then # Captured once, then grepped via here-string — see the pipefail/SIGPIPE # note on svc_state() above; fc-list's output is exactly what triggers it. fc_out=$(fc-list) if grep -qi "Material Symbols Outlined" <<< "$fc_out"; then pass "Material Symbols Outlined installed" else fail "Material Symbols Outlined not found — install 'ttf-material-symbols-variable' (AUR); every glyph icon in the bar/panels will render as fallback text without it" fi if grep -qi "CaskaydiaCove Nerd Font Mono" <<< "$fc_out"; then pass "CaskaydiaCove Nerd Font Mono installed" else fail "CaskaydiaCove Nerd Font Mono not found — install a Cascadia Code Nerd Font package; clock/labels/window title use it" fi else warn "fc-list not found (fontconfig missing?) — can't verify fonts" fi # --- 2. Backing services for native Quickshell modules ---------------------- # These modules talk to system services directly over D-Bus rather than # shelling out — the service being up is the actual dependency, not a CLI. section "Backing services (native QML modules)" st=$(svc_state NetworkManager.service) case "$st" in active) pass "NetworkManager.service active — Quickshell.Networking (NetworkIndicator)" ;; missing) fail "NetworkManager not installed — required by NetworkIndicator.qml" ;; *) fail "NetworkManager.service is $st — run: sudo systemctl enable --now NetworkManager" ;; esac st=$(svc_state bluetooth.service) case "$st" in active) pass "bluetooth.service active — Quickshell.Bluetooth (BluetoothIndicator)" ;; missing) fail "bluez not installed — required by BluetoothIndicator.qml" ;; *) fail "bluetooth.service is $st — run: sudo systemctl enable --now bluetooth" ;; esac # Pipewire/wireplumber are user-session services, not system ones. st=$(svc_state pipewire.service --user) case "$st" in active) pass "pipewire.service (user) active — Quickshell.Services.Pipewire (Volume/VolumeOsd)" ;; missing) fail "pipewire not installed — required by Volume.qml/VolumeOsd.qml" ;; *) fail "pipewire.service (user) is $st — run: systemctl --user enable --now pipewire pipewire-pulse wireplumber" ;; esac st=$(svc_state wireplumber.service --user) case "$st" in active) pass "wireplumber.service (user) active" ;; missing) warn "wireplumber not installed — pipewire falls back to pipewire-media-session if present, otherwise volume control may misbehave" ;; *) fail "wireplumber.service (user) is $st — run: systemctl --user enable --now wireplumber" ;; esac # --- 3. External CLIs shelled out to via Process ---------------------------- section "External CLIs (Quickshell.Io Process calls)" check_bin() { # $1 = binary, $2 = which module(s) use it, $3 = fail|warn on missing if command -v "$1" >/dev/null 2>&1; then pass "$1 found — used by $2" elif [ "$3" = "warn" ]; then warn "$1 not found — used by $2 (optional on this machine)" else fail "$1 not found — used by $2" fi } check_bin nmcli "NetworkIndicator.qml (DNS provider panel, connection queries)" fail check_bin ip "NetworkIndicator.qml (gateway/IP lookups)" fail check_bin ping "NetworkIndicator.qml (latency/packet-loss stat)" fail check_bin cat "NetworkIndicator.qml (/sys/class/net throughput counters)" fail check_bin systemctl "PowerButton.qml (suspend/hibernate/reboot/poweroff)" fail check_bin tailscale "TailscaleIndicator.qml / VpnExclusivity" warn check_bin mullvad "VpnIndicator.qml / VpnExclusivity" warn check_bin protonvpn "VpnIndicator.qml / VpnExclusivity" warn have_tailscale=0; have_mullvad=0; have_proton=0 command -v tailscale >/dev/null 2>&1 && have_tailscale=1 command -v mullvad >/dev/null 2>&1 && have_mullvad=1 command -v protonvpn >/dev/null 2>&1 && have_proton=1 if [ $have_mullvad -eq 0 ] && [ $have_proton -eq 0 ]; then warn "neither Mullvad nor ProtonVPN CLI is installed — VpnIndicator.qml will show both as 'Not installed'" fi # --- 4. VPN daemons + functional status (Mullvad/ProtonVPN/Tailscale) ------- # The exclusivity logic (VpnExclusivity.qml backing off Tailscale when a # commercial VPN connects) is pure QML and ships with the dotfiles — it # needs no setup of its own. What varies per machine is whether these three # are actually installed, enabled, and callable without a password prompt. section "VPN dual-setup (Mullvad/ProtonVPN + Tailscale)" if [ $have_tailscale -eq 1 ]; then st=$(svc_state tailscaled.service) case "$st" in active) pass "tailscaled.service is active" ;; missing) fail "tailscaled.service not found — reinstall tailscale" ;; *) fail "tailscaled.service is $st — run: sudo systemctl enable --now tailscaled" ;; esac if out=$(timeout 5 tailscale status --json 2>&1); then if python3 -c "import json,sys; json.load(sys.stdin)" >/dev/null 2>&1 <<< "$out"; then pass "tailscale status --json returns valid JSON" else warn "tailscale status --json returned non-JSON output" fi else fail "tailscale status --json failed: $(echo "$out" | head -1)" fi fi if [ $have_mullvad -eq 1 ]; then st=$(svc_state mullvad-daemon.service) case "$st" in active) pass "mullvad-daemon.service is active" ;; missing) fail "mullvad-daemon.service not found — reinstall the Mullvad package" ;; *) fail "mullvad-daemon.service is $st — run: sudo systemctl enable --now mullvad-daemon" ;; esac if out=$(timeout 5 mullvad status --json 2>&1); then if python3 -c "import json,sys; json.load(sys.stdin)" >/dev/null 2>&1 <<< "$out"; then pass "mullvad status --json returns valid JSON" else warn "mullvad status --json returned non-JSON output" fi else fail "mullvad status --json failed: $(echo "$out" | head -1)" fi fi if [ $have_proton -eq 1 ]; then # ProtonVPN's CLI drives connections through NetworkManager rather than # its own daemon — already checked above, just re-verified here in context. if out=$(timeout 5 protonvpn status 2>&1); then if grep -qi "^Status:" <<< "$out"; then pass "protonvpn status returns a parseable 'Status:' line" else warn "protonvpn status has no 'Status:' line — applyProtonStatus() in VpnIndicator.qml may misread it: $(echo "$out" | head -1)" fi else fail "protonvpn status failed: $(echo "$out" | head -1)" fi fi ts_up=0; mv_up=0; pv_up=0 # Same capture-then-here-string reasoning as above — these outputs are small # enough that the SIGPIPE race is unlikely, but there's no upside to relying # on that instead of just being consistent. [ $have_tailscale -eq 1 ] && grep -q '"BackendState":"Running"' <<< "$(tailscale status --json 2>/dev/null)" && ts_up=1 [ $have_mullvad -eq 1 ] && grep -qi "^Connected" <<< "$(mullvad status 2>/dev/null)" && mv_up=1 [ $have_proton -eq 1 ] && grep -qi "^Status:\s*Connected" <<< "$(protonvpn status 2>/dev/null)" && pv_up=1 active_count=$((ts_up + mv_up + pv_up)) if [ $active_count -le 1 ]; then pass "at most one tunnel active right now (tailscale=$ts_up mullvad=$mv_up protonvpn=$pv_up)" else warn "more than one tunnel is up right now (tailscale=$ts_up mullvad=$mv_up protonvpn=$pv_up) — bring extras down manually; quickshell only enforces exclusivity when you toggle from its own UI" fi # --- 5. System tray (currently disabled in Bar.qml) ------------------------- section "System tray (Tray.qml)" if grep -q "^\s*//\s*Tray\s*{}" "$QS_DIR/modules/bar/Bar.qml" 2>/dev/null; then warn "Tray {} is commented out in Bar.qml — skipping icon-theme check (nothing to render)" elif [ -f "$QS_DIR/modules/bar/Bar.qml" ]; then if command -v gtk-update-icon-cache >/dev/null 2>&1 || [ -d /usr/share/icons/hicolor ]; then pass "an icon theme (at least hicolor) is present for Quickshell.Services.SystemTray icon lookups" else warn "no icon theme found — tray icons may render blank" fi fi # --- Summary ----------------------------------------------------------------- section "Summary" printf " ${c_green}%d passed${c_reset}, ${c_yellow}%d warnings${c_reset}, ${c_red}%d failed${c_reset}\n" "$PASS" "$WARN" "$FAIL" if [ $FAIL -gt 0 ]; then exit 1 fi exit 0