commits
tags
#!/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"