diff --git a/dotfiles/.config/quickshell/scripts/CheckQuickshellSetup.sh b/dotfiles/.config/quickshell/scripts/CheckQuickshellSetup.sh new file mode 100755 index 0000000..1c8f61c --- /dev/null +++ b/dotfiles/.config/quickshell/scripts/CheckQuickshellSetup.sh @@ -0,0 +1,259 @@ +#!/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