foxygit / dotfiles Log in
commits tags

/install.sh · 36.58 KB

raw
#!/bin/bash
# Installerar hela miljön (sxwm, sxbar, st, konfig, skript ...).
#
#   ./install.sh          kör installationen
#   ./install.sh --demo   visar bara gränssnittet med låtsassteg (rör ingenting)
#
# Alla frågor ställs först; sedan körs installationen utan avbrott med en
# progressbar. Detaljerad utdata (apt, make, git ...) hamnar i $LOG_FILE.
set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SXWM_REPO="https://github.com/uint23/sxwm.git"
SXBAR_REPO="https://git.kristoffersson.info/repos/sxbar.git"
ST_REPO="https://git.suckless.org/st"
BUILD_DIR="/tmp/sxwm-sxbar-build"
LOG_FILE="/tmp/dotfiles-install.log"

DEMO=""
[ "${1:-}" = "--demo" ] && DEMO=1

# UTF-8 så att ${#str} räknar tecken (inte byte) vid centrering av loggan.
export LC_ALL=C.UTF-8
export DEBIAN_FRONTEND=noninteractive
# ip/rfkill ligger i sbin, som inte alltid finns i PATH för vanliga användare.
export PATH="$PATH:/usr/sbin:/sbin"
# Steg körs utan tangentbord -- fail hellre än att hänga på ett dolt lösenordsfrågor.
export GIT_TERMINAL_PROMPT=0

# ════════════════════════════════════════════════════════════════════════
#  Gränssnitt
# ════════════════════════════════════════════════════════════════════════

[ -t 1 ] && [ -t 0 ] && INTERACTIVE=1 || INTERACTIVE=0

# Gruvbox-palett (Linux-konsolen approximerar truecolor till närmaste 16 färger).
fg() { printf '\e[38;2;%s;%s;%sm' "$1" "$2" "$3"; }
RESET=$'\e[0m'
BOLD=$'\e[1m'
ORANGE=$(fg 254 128 25)
YELLOW=$(fg 250 189 47)
GREEN=$(fg 184 187 38)
RED=$(fg 251 73 52)
FG=$(fg 235 219 178)
GRAY=$(fg 146 131 116)
DARK=$(fg 80 73 69)

# Ingen terminal (pipe/logg) -- inga färgkoder.
if [ "$INTERACTIVE" = 0 ]; then
    RESET=""; BOLD=""; ORANGE=""; YELLOW=""; GREEN=""; RED=""; FG=""; GRAY=""; DARK=""
fi

# Konsolfonten på TTY saknar braille och bockar -- använd ASCII där.
if [ "$TERM" = "linux" ]; then
    SPINNER=('-' '\' '|' '/')
    SYM_OK="+"; SYM_FAIL="x"; SYM_ASK=">"
else
    SPINNER=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
    SYM_OK="✓"; SYM_FAIL="✗"; SYM_ASK="❯"
fi

LOGO=(
"███    ███ ██████  ███████  ██████  ██   ██ "
"████  ████ ██   ██ ██      ██    ██  ██ ██  "
"██ ████ ██ ██████  █████   ██    ██   ███   "
"██  ██  ██ ██   ██ ██      ██    ██  ██ ██  "
"██      ██ ██   ██ ██       ██████  ██   ██ "
)
TAGLINE="sxwm  ·  sxbar  ·  st  ·  dotfiles"

LOGO_W=0
for l in "${LOGO[@]}"; do
    [ "${#l}" -gt "$LOGO_W" ] && LOGO_W=${#l}
done

# Räknar om allt som beror på terminalens storlek. Körs vid start och vid
# varje storleksändring (sxwm tilear om st-fönstret efter att det startat).
compute_layout() {
    COLS=$(tput cols 2>/dev/null || echo 80)
    ROWS=$(tput lines 2>/dev/null || echo 24)
    SHOW_LOGO=0
    [ "$COLS" -ge $((LOGO_W + 4)) ] && [ "$ROWS" -ge 22 ] && SHOW_LOGO=1
    BLOCK_W=$(( COLS - 4 ))
    [ "$BLOCK_W" -gt 64 ] && BLOCK_W=64
    MARGIN=$(( (COLS - BLOCK_W) / 2 ))
    MARGIN_PAD=$(printf '%*s' "$MARGIN" '')
}
compute_layout
RESIZED=0
trap 'RESIZED=1' WINCH

# put_center RAD "färgad text" synlig-längd
put_center() {
    local col=$(( (COLS - $3) / 2 + 1 ))
    [ "$col" -lt 1 ] && col=1
    printf '\e[%d;%dH\e[2K%s' "$1" "$col" "$2"
}

# Skriver loggan (rad 'top' och neråt) med orange->gul toning. Sätter
# STATUS_ROW till första lediga rad under headern.
draw_header() {
    local top=$1 i r g b n=${#LOGO[@]} row=$1
    printf '\e[2J\e[H'
    if [ "$SHOW_LOGO" = 1 ]; then
        for i in "${!LOGO[@]}"; do
            r=$(( 254 + (250 - 254) * i / (n - 1) ))
            g=$(( 128 + (189 - 128) * i / (n - 1) ))
            b=$((  25 + ( 47 -  25) * i / (n - 1) ))
            put_center "$row" "$(fg $r $g $b)${LOGO[$i]}${RESET}" "$LOGO_W"
            row=$((row + 1))
        done
        row=$((row + 1))
        put_center "$row" "${GRAY}${TAGLINE}${RESET}" "${#TAGLINE}"
        row=$((row + 2))
    else
        put_center "$row" "${BOLD}${ORANGE}dotfiles${RESET} ${GRAY}install${RESET}" 16
        row=$((row + 2))
    fi
    STATUS_ROW=$row
    printf '\e[%d;1H' "$row"
}

# ask VARIABEL "Titel" "Beskrivning" -- ja/nej med ett tangenttryck (default nej).
ask() {
    local var=$1 title=$2 desc=$3 key="" ans=0 line
    printf '%s%s%s%s %s%s%s %s[j/N]%s ' "$MARGIN_PAD" "$ORANGE" "$SYM_ASK" "$RESET" \
        "$BOLD" "$title" "$RESET" "$GRAY" "$RESET"
    if [ "$INTERACTIVE" = 1 ]; then
        read -rsn1 key </dev/tty || key=""
    fi
    case "$key" in
        j|J|y|Y) ans=1; printf '%sJa%s\n' "$GREEN" "$RESET" ;;
        *)       printf '%sNej%s\n' "$GRAY" "$RESET" ;;
    esac
    while IFS= read -r line; do
        printf '%s  %s%s%s\n' "$MARGIN_PAD" "$GRAY" "$line" "$RESET"
    done < <(fold -s -w $((BLOCK_W - 2)) <<<"$desc")
    printf -v "$var" '%s' "$ans"
}

# ════════════════════════════════════════════════════════════════════════
#  WiFi-guide (iwd + dhcpcd) -- interaktiv, körs före själva installationen
#  eftersom apt behöver nät. Vid --demo används låtsasadaptrar/-nätverk.
# ════════════════════════════════════════════════════════════════════════

strip_ansi() { sed -e 's/\x1b\[[0-9;?]*[A-Za-z]//g'; }

# wifi_do "beskrivning" kommando... -- kör kommandot (utdata till loggen) och
# ersätter ".. beskrivning" med bock/kryss på samma rad. Returnerar dess status.
wifi_do() {
    local desc=$1 rc=0 sym; shift
    printf '%s  %s..%s %s' "$MARGIN_PAD" "$YELLOW" "$RESET" "$desc"
    if [ -n "$DEMO" ]; then
        sleep 0.6
        # låtsasdata behövs för menyn, så skanningen "körs" även i demo
        if [ "$1" = wifi_scan ]; then wifi_scan "$2"; fi
    elif ! "$@" >>"$LOG_FILE" 2>&1 </dev/null; then
        rc=1
    fi
    if [ "$rc" = 0 ]; then sym="${GREEN}${SYM_OK}"; else sym="${RED}${SYM_FAIL}"; fi
    printf '\r\e[2K%s  %s%s %s\n' "$MARGIN_PAD" "$sym" "$RESET" "$desc"
    return "$rc"
}

wifi_say() { printf '%s  %s%s%s\n' "$MARGIN_PAD" "$GRAY" "$1" "$RESET"; }

# wifi_prompt "text" -> svar i REPLY (radinmatning, synlig markör)
wifi_prompt() {
    printf '\n%s%s%s%s %s%s%s ' "$MARGIN_PAD" "$ORANGE" "$SYM_ASK" "$RESET" "$BOLD" "$1" "$RESET"
    printf '\e[?25h'
    read -r REPLY </dev/tty || REPLY=""
    printf '\e[?25l'
}

wifi_ifaces() {
    if [ -n "$DEMO" ]; then printf '%s\n' wlan0 wlp2s0; return 0; fi
    local d
    for d in /sys/class/net/*; do
        if [ -d "$d/wireless" ]; then basename "$d"; fi
    done
    return 0
}

wifi_mac() {
    if [ -n "$DEMO" ]; then echo "aa:bb:cc:dd:ee:0${#1}"; return 0; fi
    cat "/sys/class/net/$1/address" 2>/dev/null || true
}

# Skannar och fyller NET_NAMES/NET_SEC/NET_SIG (i nuvarande skal, inte subshell).
wifi_scan() {
    local iface=$1 line
    NET_NAMES=(); NET_SEC=(); NET_SIG=()
    if [ -n "$DEMO" ]; then
        NET_NAMES=("Emmeli" "Telia-4F2A" "Grannens WiFi" "Cafe Guest")
        NET_SEC=(psk psk psk open); NET_SIG=('****' '***' '**' '*')
        return 0
    fi
    sudo iwctl station "$iface" scan || true
    sleep 3
    while IFS= read -r line; do
        if [[ $line =~ ^[[:space:]]*([>][[:space:]]+)?(.*[^[:space:]])[[:space:]]{2,}(open|psk|8021x)[[:space:]]+(\**) ]]; then
            NET_NAMES+=("${BASH_REMATCH[2]}")
            NET_SEC+=("${BASH_REMATCH[3]}")
            NET_SIG+=("${BASH_REMATCH[4]}")
        fi
    done < <(sudo iwctl station "$iface" get-networks 2>/dev/null | strip_ansi)
    return 0
}

# wifi_connect IFACE SSID [LÖSENORD] [dolt]
wifi_connect() {
    local iface=$1 ssid=$2 pass=${3:-} hidden=${4:-} cmd=connect
    [ -n "$hidden" ] && cmd=connect-hidden
    if [ -n "$pass" ]; then
        sudo iwctl --passphrase "$pass" station "$iface" "$cmd" "$ssid"
    else
        sudo iwctl station "$iface" "$cmd" "$ssid"
    fi
}

wifi_wait_connected() {
    local n
    for n in 1 2 3 4 5 6 7 8 9 10; do
        if sudo iwctl station "$1" show | strip_ansi | grep -qE 'State[[:space:]]+connected'; then
            return 0
        fi
        sleep 1
    done
    return 1
}

wifi_wait_ip() {
    local n
    for n in $(seq 1 25); do
        if ip -4 -o addr show dev "$1" | grep -q inet; then return 0; fi
        sleep 1
    done
    return 1
}

WIFI_SUMMARY=""

wifi_setup() {
    local iface="" ifaces=() ssid pass sec hidden k n ip had_dhcpcd=0

    draw_header 2
    printf '\n%s%sWiFi%s %s(iwd + dhcpcd)%s\n\n' "$MARGIN_PAD" "$BOLD$ORANGE" "$RESET" "$GRAY" "$RESET"

    if [ -z "$DEMO" ]; then
        dpkg -s dhcpcd >/dev/null 2>&1 && had_dhcpcd=1
        if ! dpkg -s iwd dhcpcd >/dev/null 2>&1; then
            if ! wifi_do "Installerar iwd och dhcpcd" sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y iwd dhcpcd; then
                wifi_say "Kunde inte installera paketen (ingen nätverksanslutning än?). Hoppar över WiFi."
                sleep 3; return 1
            fi
        fi
        if systemctl is-active --quiet NetworkManager; then
            wifi_say "Obs: NetworkManager körs och kan krocka med iwd/dhcpcd."
        fi
    fi
    wifi_do "Startar iwd" sudo systemctl enable --now iwd || true
    if command -v rfkill >/dev/null 2>&1; then wifi_do "Låser upp radion (rfkill)" sudo rfkill unblock wifi || true; fi
    sleep 1

    # --- Välj adapter ---
    mapfile -t ifaces < <(wifi_ifaces)
    if [ "${#ifaces[@]}" -eq 0 ]; then
        wifi_say "Hittade ingen WiFi-adapter. Hoppar över WiFi."
        sleep 3; return 1
    elif [ "${#ifaces[@]}" -eq 1 ]; then
        iface=${ifaces[0]}
        wifi_say "Adapter: $iface (den enda hittade)"
    else
        printf '\n%s%sVälj WiFi-adapter%s\n' "$MARGIN_PAD" "$BOLD" "$RESET"
        for k in "${!ifaces[@]}"; do
            printf '%s  %s%d)%s %-18s %s%s%s\n' "$MARGIN_PAD" "$ORANGE" $(( k + 1 )) "$RESET" \
                "${ifaces[$k]}" "$GRAY" "$(wifi_mac "${ifaces[$k]}")" "$RESET"
        done
        while :; do
            wifi_prompt "Adapter [1-${#ifaces[@]}], s = hoppa över:"
            case "$REPLY" in
                s|S) return 1 ;;
                ''|*[!0-9]*) ;;
                *) if [ "$REPLY" -ge 1 ] && [ "$REPLY" -le "${#ifaces[@]}" ]; then
                       iface=${ifaces[$(( REPLY - 1 ))]}; break
                   fi ;;
            esac
        done
        printf '\n'
    fi

    # --- Välj nätverk och anslut (loopar tills det lyckas eller man hoppar över) ---
    while :; do
        wifi_do "Söker efter nätverk på $iface" wifi_scan "$iface" || true
        printf '\n%s%sNätverk%s\n' "$MARGIN_PAD" "$BOLD" "$RESET"
        n=${#NET_NAMES[@]}; [ "$n" -gt 15 ] && n=15
        for (( k = 0; k < n; k++ )); do
            printf '%s  %s%2d)%s %-30s %s%-5s %s%s\n' "$MARGIN_PAD" "$ORANGE" $(( k + 1 )) "$RESET" \
                "${NET_NAMES[$k]:0:30}" "$GRAY" "${NET_SEC[$k]}" "${NET_SIG[$k]}" "$RESET"
        done
        [ "$n" -eq 0 ] && wifi_say "Inga nätverk hittades."
        wifi_prompt "Nätverk [1-$n], r = skanna om, m = dolt nätverk, s = hoppa över:"

        hidden=""; ssid=""; sec=psk
        case "$REPLY" in
            s|S) return 1 ;;
            r|R|'') continue ;;
            m|M) printf '%s  SSID: ' "$MARGIN_PAD"; printf '\e[?25h'
                 read -r ssid </dev/tty || ssid=""; printf '\e[?25l'
                 [ -z "$ssid" ] && continue
                 hidden=1 ;;
            *[!0-9]*) continue ;;
            *) if [ "$REPLY" -ge 1 ] && [ "$REPLY" -le "$n" ]; then
                   ssid=${NET_NAMES[$(( REPLY - 1 ))]}; sec=${NET_SEC[$(( REPLY - 1 ))]}
               else continue; fi ;;
        esac

        if [ "$sec" = 8021x ]; then
            wifi_say "\"$ssid\" använder 802.1x (företagsnät) -- konfigurera det manuellt med iwctl."
            continue
        fi

        pass=""
        if [ "$sec" != open ]; then
            printf '\n%s  Lösenord för %s%s%s: ' "$MARGIN_PAD" "$BOLD" "$ssid" "$RESET"
            printf '\e[?25h'
            read -rs pass </dev/tty || pass=""
            printf '\e[?25l\n'
            # dolt öppet nätverk: tomt lösenord är tillåtet
            [ -z "$pass" ] && [ -z "$hidden" ] && continue
        fi

        if wifi_do "Ansluter till $ssid" wifi_connect "$iface" "$ssid" "$pass" "$hidden" \
            && wifi_do "Väntar på association" wifi_wait_connected "$iface"; then
            break
        fi
        wifi_say "Anslutningen misslyckades (fel lösenord eller för svag signal?). Försöker igen."
        pass=""
    done

    # --- dhcpcd bara på vald adapter ---
    if [ -z "$DEMO" ] && systemctl is-active --quiet dhcpcd.service; then
        wifi_say "dhcpcd körs redan på alla adaptrar -- lämnar det orört."
    else
        # Debian aktiverar dhcpcd.service (alla adaptrar) vid installation; det
        # skulle ta över även kabelnätet, så stäng av det om vi nyss installerade det.
        if [ "$had_dhcpcd" = 0 ]; then
            wifi_do "Stänger av dhcpcd på övriga adaptrar" sudo systemctl disable --now dhcpcd.service || true
        fi
        wifi_do "Startar dhcpcd på $iface" sudo systemctl enable --now "dhcpcd@${iface}.service" || true
    fi

    if wifi_do "Väntar på IP-adress" wifi_wait_ip "$iface"; then
        if [ -n "$DEMO" ]; then ip="192.168.1.42/24"
        else ip=$(ip -4 -o addr show dev "$iface" | awk '{print $4}' | head -1); fi
        WIFI_SUMMARY="WiFi: ${ssid} på ${iface} (${ip})"
        wifi_do "Kontrollerar namnuppslagning" getent hosts deb.debian.org || wifi_say "DNS svarar inte än -- kolla /etc/resolv.conf efter installationen."
    else
        WIFI_SUMMARY="WiFi: ansluten till ${ssid} på ${iface}, men ingen IP-adress fick ännu (se 'journalctl -u dhcpcd@${iface}')"
        wifi_say "Ingen IP-adress fick. Installationen fortsätter ändå."
    fi
    sleep 2
    return 0
}

# ════════════════════════════════════════════════════════════════════════
#  Steg
# ════════════════════════════════════════════════════════════════════════

step_deps() {
    # apt update returnerar fel om ett tredjepartsförråd (t.ex. VS Code) har
    # trasig signatur -- det ska inte stoppa hela installationen.
    sudo env DEBIAN_FRONTEND=noninteractive apt-get update -qq \
        || echo "VARNING: apt-get update misslyckades (trasigt förråd?), fortsätter"
    sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y \
        git \
        make \
        gcc \
        build-essential \
        pkg-config \
        libx11-dev \
        libxinerama-dev \
        libxrandr-dev \
        libxcursor-dev \
        libxft-dev \
        libfontconfig1-dev \
        libimlib2-dev \
        xorg \
        xinit \
        firefox-esr \
        pipewire-audio \
        alsa-utils \
        bluez \
        iwd \
        xinput \
        patch \
        wget \
        curl \
        unzip \
        fontconfig \
        wmctrl \
        xdotool \
        x11-utils \
        pipx \
        feh \
        dmenu \
        imagemagick \
        bc \
        geany \
        gocryptfs \
        fuse3
}

step_sxwm() {
    mkdir -p "$BUILD_DIR"
    cd "$BUILD_DIR"
    rm -rf sxwm
    git clone --depth=1 "$SXWM_REPO" sxwm

    cd sxwm
    patch -p1 < "$SCRIPT_DIR/cursor-dpi-fix.patch"
    # Lägger till stöd för _NET_ACTIVE_WINDOW (behövs för sxbars taskbar-modul
    # -- utan denna gör klick i taskbaren ingenting, sxwm har ingen annan
    # extern mekanism för att fokusera ett specifikt fönster)
    patch -p1 < "$SCRIPT_DIR/net-active-window.patch"
    # Fixar _NET_WM_STRUT_PARTIAL-hantering vid monitorer av olika storlek --
    # reservationer band tidigare bara till varje monitors x/y-spann, inte
    # till panelfönstrets egen monitor, vilket kunde göra en monitors
    # reserverade yta större än dess egen höjd/bredd (fönster öppnades då
    # långt utanför synligt område, bara ram synlig)
    patch -p1 < "$SCRIPT_DIR/multi-monitor-struts.patch"
    # Windows tappar rätt monitor-koppling vid hotplug (t.ex. lid-display-watch
    # som slår av/på eDP) -- client->mon är bara ett index i mons[], och blir
    # meningslöst så fort monitor-antalet/ordningen ändras, vilket kan flytta
    # fönster till fel skärm eller klämma in dem för litet. Räknar om mon-
    # tillhörighet från varje fönsters faktiska position vid varje hotplug.
    patch -p1 < "$SCRIPT_DIR/monitor-hotplug-remap.patch"
    # apply_fullscreen() placerade fönstret på c->mon, som bara sätts en gång vid
    # map-tid (till skärmen muspekaren råkade stå på) och aldrig hölls i synk med
    # fönstrets verkliga position -- ett fönster flyttat av ett externt verktyg
    # (xdotool, screensaver-launch) till en annan skärm innan det gjordes
    # fullskärm hamnade då fullskärm på FEL skärm. Räknar om målskärmen från
    # fönstrets faktiska geometri istället.
    patch -p1 < "$SCRIPT_DIR/fullscreen-monitor-fix.patch"
    # En bottom/right-panel (t.ex. sxbars secondary_bar) kunde hamna dold bakom
    # vanliga fönster -- update_struts() räknar reserve_bottom/reserve_right som
    # avstånd från *motsatt* skärmkant och behövde då hela skärmens bredd/höjd,
    # men läste den ur scr_width/scr_height som bara uppdateras i update_mons()
    # (start + monitor-hotplug). Blir de cachade värdena inaktuella räknas bara
    # bottom/right fel (top/left beror aldrig på skärmstorleken, bara på
    # monitorns egen x/y, och påverkades inte). Läser bredd/höjd direkt från
    # X-servern i update_struts() istället för att lita på cachen.
    patch -p1 < "$SCRIPT_DIR/strut-fresh-screen-size.patch"

    make
    sudo make install
}

step_sxbar() {
    mkdir -p "$BUILD_DIR"
    cd "$BUILD_DIR"
    rm -rf sxbar
    git clone --depth=1 "$SXBAR_REPO" sxbar
    cd sxbar
    make
    sudo make install
}

step_st() {
    mkdir -p "$BUILD_DIR"
    cd "$BUILD_DIR"
    rm -rf st
    git clone --depth=1 "$ST_REPO" st

    # delkey, scrollback, clipboard, boxdraw_v2, vertcenter, alpha,
    # blinking_cursor, bold-is-not-bright, anysize, histsize-5000,
    # kitty-graphics-protocol
    cd st
    for p in "$SCRIPT_DIR"/st-patches/*.patch; do
        patch -p1 < "$p"
    done
    cp "$SCRIPT_DIR/config/st/config.def.h" config.def.h

    make
    sudo make install

    # icat: visa bilder i st via kitty graphics-protokollet
    mkdir -p "$HOME/.local/bin"
    cp icat-mini.sh "$HOME/.local/bin/icat"
    chmod +x "$HOME/.local/bin/icat"
    echo "icat-mini.sh -> ~/.local/bin/icat"
}

copy_config() {
    local src="$SCRIPT_DIR/config/$1"
    local dst="$HOME/.config/$2"
    mkdir -p "$(dirname "$dst")"
    if [ -f "$dst" ]; then
        cp "$dst" "$dst.bak"
        echo "$2 -> backup sparad: ~/.config/$2.bak"
    fi
    cp "$src" "$dst"
    echo "$2 -> ~/.config/$2"
}

step_configs() {
    mkdir -p "$HOME/.config/sxbar"
    copy_config sxwmrc sxwmrc
    copy_config sxbarc sxbar/sxbarc
    copy_config geany/geany.conf geany/geany.conf
    copy_config geany/colorschemes/gruvbox.conf geany/colorschemes/gruvbox.conf
}

step_theme() {
    # Gruvbox GTK-tema (för Geanys gränssnitt, inte bara editorn)
    mkdir -p "$HOME/.local/share/themes/Gruvbox-Dark/gtk-3.0"
    cp "$SCRIPT_DIR/themes/Gruvbox-Dark/gtk-3.0/gtk.css" "$HOME/.local/share/themes/Gruvbox-Dark/gtk-3.0/gtk.css"
    cp "$SCRIPT_DIR/themes/Gruvbox-Dark/index.theme" "$HOME/.local/share/themes/Gruvbox-Dark/index.theme"
    echo "Gruvbox-Dark -> ~/.local/share/themes/Gruvbox-Dark"

    # geany-wrapper: sätter GTK_THEME=Gruvbox-Dark oavsett hur Geany startas.
    # Ett bash-alias räcker inte -- dmenu/sxwm kör aldrig ~/.bashrc, men ärver
    # PATH från ~/.xinitrc där ~/.local/bin ligger först, så denna skuggar
    # /usr/bin/geany.
    mkdir -p "$HOME/.local/bin"
    cp "$SCRIPT_DIR/bin/geany" "$HOME/.local/bin/geany"
    chmod +x "$HOME/.local/bin/geany"
    echo "geany -> ~/.local/bin/geany"

    # .desktop-override: samma tema från app-startare
    mkdir -p "$HOME/.local/share/applications"
    cp "$SCRIPT_DIR/applications/geany.desktop" "$HOME/.local/share/applications/geany.desktop"
    echo "geany.desktop -> ~/.local/share/applications/geany.desktop"
}

step_wallpaper() {
    mkdir -p "$HOME/BG"
    cp -n "$SCRIPT_DIR/BG/"* "$HOME/BG/"
    echo "BG/ -> ~/BG/"
}

step_xinitrc() {
    if [ -f "$HOME/.xinitrc" ]; then
        cp "$HOME/.xinitrc" "$HOME/.xinitrc.bak"
        echo "Backup skapad: ~/.xinitrc.bak"
    fi
    cat > "$HOME/.xinitrc" << 'EOF'
#!/bin/bash
# ~/.bash_profile sourcar inte ~/.profile/~/.bashrc, så PATH-tillägget för
# ~/.local/bin når annars aldrig sxwm eller dess "exec"-kommandon
# (mouse-settings, lid-display-watch m.fl. failar tyst utan denna rad)
export PATH="$HOME/.local/bin:$PATH"

# Stänga av skärmarna efter 5 min
xset s 300 300
xset dpms 300 300 300

# Stäng av pip-ljudet (bell)
xset b off

# Svenskt tangentbord
setxkbmap se

exec sxwm
EOF
    chmod +x "$HOME/.xinitrc"
    echo "~/.xinitrc skapad"
}

step_scripts() {
    mkdir -p "$HOME/.local/bin"
    local s
    for s in lidsuspend mouse-settings status gcfs; do
        cp "$SCRIPT_DIR/bin/$s" "$HOME/.local/bin/$s"
        chmod +x "$HOME/.local/bin/$s"
        echo "$s -> ~/.local/bin/$s"
    done
    # gcfs: lägg bara exempel-configen om ingen redan finns -- gcfs skapar en
    # identisk vid första körning, och en riktig vaults.conf ska aldrig
    # skrivas över.
    if [ ! -f "$HOME/.config/gcfs/vaults.conf" ]; then
        mkdir -p "$HOME/.config/gcfs"
        cp "$SCRIPT_DIR/config/gcfs/vaults.conf" "$HOME/.config/gcfs/vaults.conf"
        echo "vaults.conf -> ~/.config/gcfs/"
    else
        echo "behöll befintlig ~/.config/gcfs/vaults.conf"
    fi
}

step_screensaver() {
    pipx install terminaltexteffects
    pipx ensurepath

    mkdir -p "$HOME/.local/bin"
    cp "$SCRIPT_DIR/bin/screensaver" "$SCRIPT_DIR/bin/screensaverd" "$SCRIPT_DIR/bin/screensaver-launch" "$SCRIPT_DIR/bin/screensaver-toggle" "$SCRIPT_DIR/bin/screensaver.txt" "$HOME/.local/bin/"
    chmod +x "$HOME/.local/bin/screensaver" "$HOME/.local/bin/screensaverd" "$HOME/.local/bin/screensaver-launch" "$HOME/.local/bin/screensaver-toggle"
    echo "screensaver, screensaverd, screensaver-launch, screensaver-toggle, screensaver.txt -> ~/.local/bin/"
}

step_lidwatch() {
    cp "$SCRIPT_DIR/bin/internal-display" "$HOME/.local/bin/internal-display"
    chmod +x "$HOME/.local/bin/internal-display"
    cp "$SCRIPT_DIR/bin/lid-display-watch" "$HOME/.local/bin/lid-display-watch"
    chmod +x "$HOME/.local/bin/lid-display-watch"
    echo "internal-display, lid-display-watch -> ~/.local/bin/"

    if ! grep -qF 'exec : "lid-display-watch"' "$HOME/.config/sxwmrc" 2>/dev/null; then
        echo 'exec : "lid-display-watch"' >> "$HOME/.config/sxwmrc"
        echo "lid-display-watch tillagd i ~/.config/sxwmrc"
    fi
}

step_lidswitch() {
    local LOGIND_CONF="/etc/systemd/logind.conf"
    if grep -q '^HandleLidSwitch=igonre$' "$LOGIND_CONF" 2>/dev/null; then
        # Stavfel som gjort att systemd-logind ignorerat raden och fallit
        # tillbaka på det inbyggda default-värdet ("suspend") istället
        sudo sed -i 's/^HandleLidSwitch=igonre$/HandleLidSwitch=ignore/' "$LOGIND_CONF"
        echo "HandleLidSwitch=igonre -> ignore (rättade stavfel)"
    elif grep -q '^HandleLidSwitch=' "$LOGIND_CONF" 2>/dev/null; then
        echo "HandleLidSwitch redan satt, rör inte: $(grep '^HandleLidSwitch=' "$LOGIND_CONF")"
    else
        echo 'HandleLidSwitch=ignore' | sudo tee -a "$LOGIND_CONF" > /dev/null
        echo "HandleLidSwitch=ignore tillagd"
    fi
    # Kräver omstart för att aktiveras. Kör INTE 'systemctl restart
    # systemd-logind' live -- på den här maskinen (X startas manuellt via
    # startx/.xinitrc, ingen display manager) har det hängt hela skrivbordet
    # och krävt en hård omstart.
}

step_btudev() {
    # Specifik för den här laptopens BT-chip (Qualcomm Atheros 0cf3:e500).
    # Annan hårdvara: hitta idVendor/idProduct via `lsusb`, matcha mot
    # /sys/class/bluetooth/hci0 -> realpath för att hitta rätt usb-enhet.
    sudo cp "$SCRIPT_DIR/udev/71-bluetooth-no-autosuspend.rules" /etc/udev/rules.d/71-bluetooth-no-autosuspend.rules
    sudo udevadm control --reload-rules
    sudo udevadm trigger --attr-match=idVendor=0cf3 --attr-match=idProduct=e500
    echo "71-bluetooth-no-autosuspend.rules -> /etc/udev/rules.d/"
}

step_consoleblank() {
    local GRUB_FILE="/etc/default/grub"
    if grep -q 'consoleblank=' "$GRUB_FILE" 2>/dev/null; then
        echo "consoleblank redan konfigurerad i $GRUB_FILE, hoppar över"
    else
        sudo sed -i '/^GRUB_CMDLINE_LINUX_DEFAULT=/ s/"$/ consoleblank=300"/' "$GRUB_FILE"
        sudo update-grub
        echo "consoleblank=300 tillagt (5 min), kräver omstart för att aktiveras"
    fi
}

step_nobeep() {
    echo "blacklist pcspkr" | sudo tee /etc/modprobe.d/nobeep.conf > /dev/null
    sudo rmmod pcspkr 2>/dev/null || true
}

step_rust() {
    # Ta bort ev. gammal rustc/cargo från apt (Debian har ofta för gammal version)
    local RUST_APT_PKGS
    RUST_APT_PKGS=$(dpkg-query -W -f='${Package}\n' 'rustc' 'cargo' 'libstd-rust-dev' 'rust-llvm' 'libstd-rust-*' 2>/dev/null || true)
    if [ -n "$RUST_APT_PKGS" ]; then
        sudo env DEBIAN_FRONTEND=noninteractive apt-get purge -y $RUST_APT_PKGS
        sudo env DEBIAN_FRONTEND=noninteractive apt-get autoremove --purge -y
    fi

    if ! command -v rustup >/dev/null 2>&1 && [ ! -x "$HOME/.cargo/bin/rustup" ]; then
        curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
    fi
    # shellcheck disable=SC1091
    source "$HOME/.cargo/env"

    mkdir -p "$BUILD_DIR"
    rm -rf "$BUILD_DIR/bluetui"
    git clone --depth=1 https://github.com/pythops/bluetui "$BUILD_DIR/bluetui"
    cd "$BUILD_DIR/bluetui"
    cargo build --release
    sudo cp target/release/bluetui /usr/local/bin/
}

step_font() {
    local FONT_DIR="$HOME/.local/share/fonts/JetBrainsMono"
    local FONT_URL="https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.zip"
    mkdir -p "$FONT_DIR"
    wget -q -O /tmp/JetBrainsMono.zip "$FONT_URL"
    unzip -o /tmp/JetBrainsMono.zip -d "$FONT_DIR" '*.ttf'
    rm /tmp/JetBrainsMono.zip
    fc-cache -f
    echo "JetBrainsMono Nerd Font installerad"
}

step_autostart() {
    local BASH_PROFILE="$HOME/.bash_profile"
    local AUTOSTART='if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then exec startx; fi'
    if grep -qF 'exec startx' "$BASH_PROFILE" 2>/dev/null; then
        echo "~/.bash_profile redan konfigurerad, hoppar över"
    else
        echo "" >> "$BASH_PROFILE"
        echo "$AUTOSTART" >> "$BASH_PROFILE"
        echo "~/.bash_profile uppdaterad"
    fi
}

step_pipewire() {
    systemctl --user enable --now pipewire pipewire-pulse wireplumber
}

# Låtsassteg för --demo: skriver lite "loggutdata" och väntar i proportion
# till stegets vikt. DEMO_FAIL=<del av stegnamn> låter ett steg misslyckas.
demo_step() {
    local name=$1 weight=$2 n
    for n in 1 2 3 4 5 6; do
        echo "demo: $name ($n/6) ..."
        sleep "0.$((weight > 9 ? 9 : weight))"
    done
    if [ -n "${DEMO_FAIL:-}" ] && [[ "$name" == *"$DEMO_FAIL"* ]]; then
        echo "demo: simulerat fel i '$name'"
        return 1
    fi
}

# ════════════════════════════════════════════════════════════════════════
#  Steghantering
# ════════════════════════════════════════════════════════════════════════

STEP_NAMES=(); STEP_FNS=(); STEP_W=()
TOTAL_W=0
# add_step VIKT "Namn" funktion -- vikten styr hur mycket steget flyttar baren.
add_step() {
    STEP_W+=("$1"); STEP_NAMES+=("$2"); STEP_FNS+=("$3")
    TOTAL_W=$((TOTAL_W + $1))
}

KEEPALIVE_PID=""
cleanup() {
    local rc=$?
    [ -n "$KEEPALIVE_PID" ] && kill "$KEEPALIVE_PID" 2>/dev/null
    kill $(jobs -p) 2>/dev/null || true
    printf '\e[?25h%s\n' "$RESET"
    exit "$rc"
}
trap cleanup EXIT
trap 'exit 130' INT TERM

# Senaste raderna ur loggen, utan färgkoder/CR och blanka rader.
log_tail() {
    tail -c 4000 "$LOG_FILE" 2>/dev/null | tr '\r\t' '\n ' \
        | sed -e 's/\x1b\[[0-9;?]*[A-Za-z]//g' | grep -v '^[[:space:]]*$' | tail -n 3
}

fmt_time() { printf '%02d:%02d' $(( $1 / 60 )) $(( $1 % 60 )); }

# render PROCENT "steg-namn" steg-nr spinner-tick "lograder"
render() {
    local pct=$1 label=$2 idx=$3 tick=$4 logs=$5
    local bar_w=$(( COLS - 16 )) filled f e row=$STATUS_ROW line
    [ "$bar_w" -gt 46 ] && bar_w=46
    filled=$(( pct * bar_w / 100 ))
    printf -v f '%*s' "$filled" ''; f=${f// /█}
    printf -v e '%*s' $(( bar_w - filled )) ''; e=${e// /░}
    put_center "$row" "${ORANGE}${f}${DARK}${e}${RESET}  ${FG}$(printf '%3d%%' "$pct")${RESET}" $(( bar_w + 6 ))

    local spin=${SPINNER[$(( tick % ${#SPINNER[@]} ))]}
    local meta="  ${idx}/${#STEP_FNS[@]} · $(fmt_time "$SECONDS")"
    put_center $(( row + 2 )) "${YELLOW}${spin}${RESET} ${BOLD}${FG}${label}${RESET}${GRAY}${meta}${RESET}" $(( 2 + ${#label} + ${#meta} ))

    row=$(( row + 4 ))
    local i=0
    while IFS= read -r line; do
        printf '\e[%d;%dH\e[2K%s%s%s' "$row" $(( MARGIN + 1 )) "$DARK" "${line:0:$BLOCK_W}" "$RESET"
        row=$(( row + 1 )); i=$(( i + 1 ))
    done <<<"$logs"
    while [ "$i" -lt 3 ]; do
        printf '\e[%d;1H\e[2K' "$row"; row=$(( row + 1 )); i=$(( i + 1 ))
    done
}

fail_screen() {
    local name=$1
    printf '\e[%d;1H\e[J' "$STATUS_ROW"
    printf '%s%s%s %sSteget misslyckades:%s %s\n\n' "$MARGIN_PAD" "$RED" "$SYM_FAIL" "$BOLD" "$RESET" "$name"
    tail -n 12 "$LOG_FILE" | sed -e "s/^/${MARGIN_PAD}${GRAY}/" -e "s/\$/${RESET}/"
    printf '\n%sFullständig logg: %s%s\n' "$MARGIN_PAD" "$LOG_FILE" "$RESET"
}

run_steps() {
    local i fn name weight pid rc tick done_w=0 pct creep
    local total=${#STEP_FNS[@]}
    for i in "${!STEP_FNS[@]}"; do
        fn=${STEP_FNS[$i]}; name=${STEP_NAMES[$i]}; weight=${STEP_W[$i]}
        printf '\n=== [%d/%d] %s ===\n' $(( i + 1 )) "$total" "$name" >>"$LOG_FILE"

        # Körs inte i ett ||/if-sammanhang, så `set -e` gäller inuti steget.
        if [ -n "$DEMO" ]; then
            ( demo_step "$name" "$weight" ) >>"$LOG_FILE" 2>&1 </dev/null &
        else
            ( set -e; "$fn" ) >>"$LOG_FILE" 2>&1 </dev/null &
        fi
        pid=$!

        tick=0; rc=0
        if [ "$PLAIN" = 1 ]; then
            echo "==> $name"
            wait "$pid" || rc=$?
        else
            local logs=""
            while kill -0 "$pid" 2>/dev/null; do
                if [ "$RESIZED" = 1 ]; then
                    RESIZED=0; compute_layout; draw_run_header
                fi
                # Baren kryper asymptotiskt mot 90 % av stegets vikt så att den
                # rör sig även under långa steg (apt, cargo) utan att ljuga.
                creep=$(( 900 * tick / (tick + 200) ))
                pct=$(( (done_w * 1000 + weight * creep) / (TOTAL_W * 10) ))
                [ $(( tick % 3 )) -eq 0 ] && logs=$(log_tail)
                render "$pct" "$name" $(( i + 1 )) "$tick" "$logs"
                tick=$(( tick + 1 ))
                sleep 0.1
            done
            wait "$pid" || rc=$?
        fi

        if [ "$rc" -ne 0 ]; then
            if [ "$PLAIN" = 1 ]; then
                echo "Steget misslyckades: $name (se $LOG_FILE)"
            else
                fail_screen "$name"
            fi
            exit 1
        fi
        done_w=$(( done_w + weight ))
    done
    [ "$PLAIN" = 1 ] || render 100 "Klart" "$total" 0 ""
}

# ════════════════════════════════════════════════════════════════════════
#  Main
# ════════════════════════════════════════════════════════════════════════

PLAIN=0
[ "$INTERACTIVE" = 1 ] || PLAIN=1

: > "$LOG_FILE"

if [ "$PLAIN" = 0 ]; then
    printf '\e[?25l'
    draw_header 2
    printf '\n'
    printf '%s%sKonfigurera installationen%s %s(ett tangenttryck per fråga)%s\n\n' \
        "$MARGIN_PAD" "$BOLD$ORANGE" "$RESET" "$GRAY" "$RESET"
else
    echo "Kör utan terminal -- alla val faller tillbaka på 'nej'."
fi

ask A_WIFI "Konfigurera WiFi (iwd + dhcpcd)?" \
    "Väljer adapter och nätverk interaktivt, ansluter med iwctl och kör dhcpcd på vald adapter. Görs före installationen så att apt har nät."
ask A_XINITRC "Skriva över ~/.xinitrc?" \
    "Backup sparas som ~/.xinitrc.bak. Sätter PATH, skärmsläckning, svenskt tangentbord och startar sxwm."
ask A_LIDWATCH "Installera lid-display-watch?" \
    "Stänger av interna skärmen när locket stängs, men bara om en extern skärm är ansluten."
ask A_LIDSWITCH "Sätta HandleLidSwitch=ignore?" \
    "Ändrar /etc/systemd/logind.conf så att stängt lock inte suspend:ar. Kräver omstart."
ask A_BTUDEV "Stänga av USB-autosuspend för Bluetooth?" \
    "udev-regel som hindrar Bluetooth-adaptern (0cf3:e500) från att tappa anslutningar."
ask A_RUST "Installera Rust och bygga bluetui?" \
    "Tar bort apt-versionen av rustc/cargo, installerar rustup och bygger bluetui (tar en stund)."
ask A_AUTOSTART "Starta X automatiskt på TTY1?" \
    "Lägger till startx i ~/.bash_profile så att sxwm startar vid inloggning."

# Sudo-lösenord en gång i förväg; hålls sedan levande under hela körningen så
# att steg som körs i bakgrunden aldrig fastnar på en dold lösenordsfråga.
if [ -z "$DEMO" ]; then
    printf '\n%s%s%s%s %sBehöver sudo för att installera paket%s\n' "$MARGIN_PAD" "$ORANGE" "$SYM_ASK" "$RESET" "$BOLD" "$RESET"
    printf '\e[?25h'
    sudo -v
    [ "$PLAIN" = 1 ] || printf '\e[?25l'
    ( while true; do sudo -n true; sleep 50; kill -0 "$$" 2>/dev/null || exit; done ) 2>/dev/null &
    KEEPALIVE_PID=$!
fi

# Interaktiv WiFi-guide i samma flöde, före stegen (apt behöver nät).
if [ "$A_WIFI" = 1 ] && [ "$PLAIN" = 0 ]; then
    wifi_setup || true
fi

add_step 10 "Installerar beroenden"        step_deps
add_step  3 "Bygger sxwm"                  step_sxwm
add_step  2 "Bygger sxbar"                 step_sxbar
add_step  4 "Bygger st"                    step_st
add_step  1 "Kopierar konfigfiler"         step_configs
add_step  1 "Installerar Geany & Gruvbox"  step_theme
add_step  1 "Kopierar bakgrunder"          step_wallpaper
[ "$A_XINITRC"   = 1 ] && add_step 1 "Skriver ~/.xinitrc"        step_xinitrc
add_step  1 "Installerar skript"           step_scripts
add_step  3 "Installerar skärmsläckare"    step_screensaver
[ "$A_LIDWATCH"  = 1 ] && add_step 1 "Installerar lid-display-watch" step_lidwatch
[ "$A_LIDSWITCH" = 1 ] && add_step 1 "Konfigurerar lock-hantering"   step_lidswitch
[ "$A_BTUDEV"    = 1 ] && add_step 1 "Konfigurerar Bluetooth (udev)" step_btudev
add_step  1 "Konfigurerar skärmsläckning på TTY"  step_consoleblank
add_step  1 "Stänger av PC-speaker"        step_nobeep
[ "$A_RUST"      = 1 ] && add_step 12 "Installerar Rust och bluetui" step_rust
add_step  2 "Installerar JetBrainsMono Nerd Font"  step_font
[ "$A_AUTOSTART" = 1 ] && add_step 1 "Konfigurerar autostart av X"   step_autostart
add_step  1 "Aktiverar PipeWire"           step_pipewire

draw_run_header() {
    local top=$(( (ROWS - 16) / 2 ))
    [ "$top" -lt 1 ] && top=1
    draw_header "$top"
}

if [ "$PLAIN" = 0 ]; then
    compute_layout
    draw_run_header
fi

SECONDS=0
run_steps

if [ "$PLAIN" = 0 ]; then
    printf '\e[%d;1H\e[J' $(( STATUS_ROW + 2 ))
    done_msg="Installation slutförd ($(fmt_time "$SECONDS"))"
    put_center $(( STATUS_ROW + 2 )) "${GREEN}${SYM_OK}${RESET} ${BOLD}${FG}${done_msg}${RESET}" $(( 2 + ${#done_msg} ))
    printf '\e[%d;1H' $(( STATUS_ROW + 4 ))
fi
printf '\n'
printf '%s%sStarta om datorn så att konsol-blankning och lock-inställningar tar effekt.%s\n' "$MARGIN_PAD" "$GRAY" "$RESET"
if [ -n "$WIFI_SUMMARY" ]; then
    printf '%s%s%s%s\n' "$MARGIN_PAD" "$GRAY" "$WIFI_SUMMARY" "$RESET"
fi
if [ "$A_XINITRC" != 1 ]; then
    printf '%s%sLägg manuellt till i ~/.xinitrc: xset s 300 300; xset dpms 300 300 300; setxkbmap se; exec sxwm%s\n' "$MARGIN_PAD" "$GRAY" "$RESET"
fi
printf '%s%sLogg: %s%s\n' "$MARGIN_PAD" "$GRAY" "$LOG_FILE" "$RESET"