07 December 2025

Debian + Fluxbox Setup (2025 refresh)

— Testing Rolling, Anti-Cinnamon Guardrails

This is a 2025 refresh of my Debian + Fluxbox notes, updated specifically for Debian Testing (rolling). The major addition this year is a set of guardrails to prevent Cinnamon from being installed during upgrades or via meta/task packages, plus a pre-flight step that removes Cinnamon if it already snuck in.

What’s new in this 2025 Testing update

  • Targets Debian Testing (rolling) rather than a fixed stable snapshot.
  • Pre-guardrail cleanup: detect and purge Cinnamon if present.
  • Two-layer anti-Cinnamon safety:
    • Hard APT pin refuses Cinnamon and its task/meta packages.
    • Lean APT defaults disable automatic Recommends/Suggests to prevent surprise DE pulls.
  • Audio modernized: media keys now use wpctl (PipeWire-native), not amixer.
  • Completes the daily-driver baseline: adds CUPS and a Debian-repo browser.

Assumptions

  • Fresh minimal install of Debian Testing.
  • You want Xorg + Fluxbox (not a full desktop environment).
  • You want NetworkManager to own networking.
  • You want a clean, stable lightweight workflow even as Testing evolves.

Install command

Save the script below as debian-fluxbox-setup-testing-2025.sh, then run:

sudo bash debian-fluxbox-setup-testing-2025.sh <username>

One-shot script (2025 Testing rolling)

#!/usr/bin/env bash
###############################################################################
# Debian Testing + Fluxbox setup helper (2025, data-driven components)
#
# PURPOSE
#   Turn a minimal Debian Testing install into "my normal workstation"
#   using a small set of data-driven components and profiles.
#
# KEY FEATURES
#   - Components are defined in a single list (COMPONENT_SPECS).
#   - Profiles: workstation (default), full, minimal, guardrails.
#   - Generic per-component flags: --no-, --enable-, --with-.
#   - Interactive wizard: --interactive.
#   - Dry-run: --dry-run (print plan, no changes).
#   - Per-user defaults config: ~/.config/debian-fluxbox-setup.conf
#
# HOW TO ADD A NEW COMPONENT / OPTION
#   1) Pick an ID (no spaces, used in flags & config), e.g. "dev-tools".
#   2) Decide:
#        - A short human description.
#        - Which apt packages to install (space-separated, or empty).
#        - An optional hook function name to run after install (or empty).
#        - Which profiles enable this by default:
#            workstation, full, minimal, guardrails
#
#   3) Add ONE LINE to COMPONENT_SPECS with this format:
#        "id|Description|pkg1 pkg2 ...|hook_function|profile1,profile2,..."
#
#      Examples:
#        - Package-only component:
#            "dev-tools|Developer tools (git, vim, htop)|git vim htop||workstation,full"
#
#        - Component with a post-install hook:
#            "my-net-tweaks|Custom network tweaks||my_net_hook|full"
#
#          and somewhere above in the script:
#            my_net_hook() {
#              # do custom sysctl, config files, etc.
#            }
#
#   4) After that, you automatically get:
#        - CLI flags:
#            --no-dev-tools        (disable)
#            --enable-dev-tools    (enable)
#            --with-dev-tools      (alias for --enable-dev-tools)
#        - Interactive prompt line.
#        - Config-file override:
#            dev-tools=true        or dev-tools=false
#        - Inclusion in any profile(s) you listed.
#
# CONFIG FILE (OPTIONAL)
#   Path: ~/.config/debian-fluxbox-setup.conf (for the user invoking sudo)
#
#   Supported keys:
#      PROFILE=                    # workstation, full, minimal, guardrails
#      =true|false         # e.g. audio=false, printing=true
#
#   Example:
#      PROFILE=minimal
#      audio=true
#      browsers=true
#
# PROFILES
#   workstation  (default)  2025 blog behavior: guardrails + X + NM + utils +
#                            bt/batt tray + PipeWire audio + printing + browsers +
#                            config + dev-tools.
#   full                    workstation + extras (vlc, libreoffice, etc.) +
#                            media-graphics, media-audio, games.
#   minimal                 guardrails + X + NM + basic utils, but:
#                            - NO audio
#                            - NO printing
#                            - NO browsers
#                            - NO bt/batt tray
#                            - NO extras
#   guardrails              Cinnamon cleanup + APT guardrails only.
#
# AUDIO BACKENDS
#   - Default in workstation/full: PipeWire stack (component: audio).
#   - Optional non-default ALSA stack (component: audio-alsa).
#   - Convenience flag:
#       --alsa / --alsa-audio  => disable PipeWire audio, enable ALSA audio.
#
# EXAMPLES
#   Default workstation profile for $SUDO_USER:
#      sudo ./debian-fluxbox-setup-2025.sh
#
#   Explicit user + full profile:
#      sudo ./debian-fluxbox-setup-2025.sh --user myuser --profile full
#
#   Minimal profile:
#      sudo ./debian-fluxbox-setup-2025.sh --user myuser --profile minimal
#
#   Guardrails only:
#      sudo ./debian-fluxbox-setup-2025.sh --profile guardrails
#
#   Drop specific components:
#      sudo ./debian-fluxbox-setup-2025.sh --profile full --no-printing --no-audio
#
#   Use ALSA audio instead of PipeWire:
#      sudo ./debian-fluxbox-setup-2025.sh --profile workstation --alsa
#
#   Dry-run (no changes, just a plan):
#      ./debian-fluxbox-setup-2025.sh --profile minimal --dry-run
#
#   Interactive:
#      sudo ./debian-fluxbox-setup-2025.sh --interactive
###############################################################################
set -euo pipefail

SCRIPT_NAME="${0##*/}"

usage() {
  cat <               Disable a component (e.g. --no-audio, --no-printing).
  --enable-           Enable a component (e.g. --enable-audio).
  --with-             Alias for --enable-.
  --extras                Alias for --enable-extras.

Audio backend convenience:
  --alsa
  --alsa-audio            Disable PipeWire audio, enable ALSA audio stack.

Other:
  -h, --help              Show this help and exit.

Config file:
  ~/.config/debian-fluxbox-setup.conf (for the user invoking sudo)
  Supported keys:
    PROFILE=
    =true|false   (e.g. audio=false, printing=true)
EOF
}

###############################################################################
# Low-level helpers
###############################################################################

os_hint() {
  if [[ -r /etc/os-release ]]; then
    if ! grep -qiE 'testing' /etc/os-release; then
      echo "[!] /etc/os-release does not appear to say 'testing'."
      echo "    Continuing anyway (you may be on a testing-derived snapshot)."
    fi
  fi
}

pre_clean_cinnamon() {
  echo "[*] Checking for existing Cinnamon install..."

  # SAFETY CHECK: Prevent purging if inside a GUI
  if [[ -n "${DISPLAY:-}" ]]; then
    echo "[!] WARNING: You appear to be running inside a graphical session (DISPLAY=$DISPLAY)."
    echo "    Purging Cinnamon while logged into it (or a DM) might crash your session."
    echo "    It is STRONGLY recommended to run this component from a TTY (Ctrl+Alt+F3)."
    echo
    read -r -p "    Press Enter to continue anyway (risk of crash), or Ctrl+C to abort." || exit 1
  fi

  local CINN_PKGS_INSTALLED
  CINN_PKGS_INSTALLED="$(
    dpkg -l 2>/dev/null | awk '{print $2}' | \
      grep -E '^cinnamon($|-)|^task-cinnamon-desktop$|^cinnamon-desktop-environment$' || true
  )"

  if [[ -n "${CINN_PKGS_INSTALLED}" ]]; then
    echo "[!] Cinnamon-related packages detected:"
    echo "${CINN_PKGS_INSTALLED}" | sed 's/^/    - /'

    echo "[*] Purging Cinnamon-related packages..."
    apt-get purge -y \
      task-cinnamon-desktop \
      cinnamon-desktop-environment \
      'cinnamon*' || true

    echo "[*] Autoremoving orphaned dependencies..."
    apt-get autoremove -y || true
    echo "[*] Cinnamon removal complete."
  else
    echo "[*] No Cinnamon packages detected."
  fi
}

setup_guardrails() {
  echo "[*] Creating APT guardrails..."
  cat > /etc/apt/apt.conf.d/99lean-desktop <<'EOF'
APT::Install-Recommends "false";
APT::Install-Suggests "false";
EOF

  cat > /etc/apt/preferences.d/no-cinnamon.pref <<'EOF'
Package: cinnamon
Pin: release *
Pin-Priority: -1

Package: cinnamon-*
Pin: release *
Pin-Priority: -1

Package: task-cinnamon-desktop
Pin: release *
Pin-Priority: -1

Package: cinnamon-desktop-environment
Pin: release *
Pin-Priority: -1
EOF
}

apt_update() {
  echo "[*] Updating apt..."
  apt-get update
}

network_post_install() {
  echo "[*] Enabling NetworkManager service..."
  systemctl enable --now NetworkManager

  echo "[*] Setting /etc/network/interfaces to loopback-only (NM-friendly)..."
  if [[ -f /etc/network/interfaces ]]; then
    cp -a /etc/network/interfaces "/etc/network/interfaces.bak.$(date +%Y%m%d%H%M%S)"
  fi
  cat > /etc/network/interfaces <<'EOF'
# NetworkManager-friendly baseline.
# Loopback only.
source /etc/network/interfaces.d/*

auto lo
iface lo inet loopback
EOF
}

setup_fluxbox_config() {
  if [[ -z "${USER_NAME:-}" || -z "${USER_HOME:-}" ]]; then
    echo "[!] Fluxbox config requested but USER_NAME/USER_HOME not set; skipping."
    return 0
  fi

  # Check which components are enabled so we can conditionally add menu entries
  # and keybindings that actually match the installed toolset.
  local utils_enabled="${COMP_ENABLED[utils]:-false}"
  local browsers_enabled="${COMP_ENABLED[browsers]:-false}"
  local network_enabled="${COMP_ENABLED[network]:-false}"
  local audio_pipewire_enabled="${COMP_ENABLED[audio]:-false}"
  local audio_alsa_enabled="${COMP_ENABLED[audio-alsa]:-false}"
  local printing_enabled="${COMP_ENABLED[printing]:-false}"

  echo "[*] Creating Fluxbox config skeleton for ${USER_NAME}..."
  install -d -m 0755 "${USER_HOME}/.fluxbox"
  chown -R "${USER_NAME}:${USER_NAME}" "${USER_HOME}/.fluxbox"

  ###########################################################################
  # Menu
  ###########################################################################
  if [[ ! -f "${USER_HOME}/.fluxbox/menu" ]]; then
    # Begin menu and always-present terminal
    cat > "${USER_HOME}/.fluxbox/menu" <<'EOF'
[begin] (fluxbox)
  [exec] (Terminal) {xterm}
EOF

    # File manager + display tools if utils=true
    if [[ "${utils_enabled}" == "true" ]]; then
      cat >> "${USER_HOME}/.fluxbox/menu" <<'EOF'
  [exec] (File Manager) {thunar}
  [exec] (Display Settings) {arandr}
EOF
    fi

    # Browsers if browsers=true
    if [[ "${browsers_enabled}" == "true" ]]; then
      cat >> "${USER_HOME}/.fluxbox/menu" <<'EOF'
  [exec] (Web Browser - Chromium) {chromium}
  [exec] (Web Browser - Firefox ESR) {firefox-esr}
EOF
    fi

    # Network connections editor if network=true
    if [[ "${network_enabled}" == "true" ]]; then
      cat >> "${USER_HOME}/.fluxbox/menu" <<'EOF'
  [exec] (Network Connections) {nm-connection-editor}
EOF
    fi

    # Audio control if any audio backend is enabled
    if [[ "${audio_pipewire_enabled}" == "true" ]]; then
      cat >> "${USER_HOME}/.fluxbox/menu" <<'EOF'
  [exec] (Audio Control) {pavucontrol}
EOF
    elif [[ "${audio_alsa_enabled}" == "true" ]]; then
      cat >> "${USER_HOME}/.fluxbox/menu" <<'EOF'
  [exec] (Audio Mixer) {alsamixer}
EOF
    fi

    # Printing GUI if printing=true
    if [[ "${printing_enabled}" == "true" ]]; then
      cat >> "${USER_HOME}/.fluxbox/menu" <<'EOF'
  [exec] (Printers) {system-config-printer}
EOF
    fi

    # System submenu (always present)
    cat >> "${USER_HOME}/.fluxbox/menu" <<'EOF'
  [submenu] (System)
    [exec] (Reconfigure) {fluxbox-remote reconfigure}
    [exec] (Restart) {fluxbox-remote restart}
    [exec] (Logout) {fluxbox-remote exit}
  [end]
[end]
EOF
  fi

  ###########################################################################
  # Keys
  ###########################################################################
  if [[ ! -f "${USER_HOME}/.fluxbox/keys" ]]; then
    # Base keybindings (non-audio)
    cat > "${USER_HOME}/.fluxbox/keys" <<'EOF'
# Basic Fluxbox keybindings

Mod1 Tab :NextWindow (workspace=[current])
Mod1 Shift Tab :PrevWindow (workspace=[current])

Mod1 F4 :Close

Mod1 z :RootMenu
Mod1 Shift z :HideMenus
Mod1 x :ExecCommand xterm

Control Shift n :ExecCommand chromium --incognito

XF86MonBrightnessUp   :ExecCommand /usr/bin/xbacklight -inc 5
XF86MonBrightnessDown :ExecCommand /usr/bin/xbacklight -dec 5
EOF

    # Audio keybindings (PipeWire) if enabled
    if [[ "${audio_pipewire_enabled}" == "true" ]]; then
      cat >> "${USER_HOME}/.fluxbox/keys" <<'EOF'

# Audio volume keys (PipeWire via wpctl)
XF86AudioLowerVolume :ExecCommand wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-
XF86AudioRaiseVolume :ExecCommand wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+
XF86AudioMute        :ExecCommand wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
EOF
    fi

    # Audio keybindings (ALSA) if enabled
    if [[ "${audio_alsa_enabled}" == "true" ]]; then
      cat >> "${USER_HOME}/.fluxbox/keys" <<'EOF'

# Audio volume keys (ALSA via amixer)
XF86AudioLowerVolume :ExecCommand amixer set Master 5%- unmute
XF86AudioRaiseVolume :ExecCommand amixer set Master 5%+ unmute
XF86AudioMute        :ExecCommand amixer set Master toggle
EOF
    fi
  fi

  ###########################################################################
  # Startup and .xinitrc
  ###########################################################################
  cat > "${USER_HOME}/.fluxbox/startup" <<'EOF'
#!/bin/sh

xset -b

command -v numlockx       >/dev/null 2>&1 && numlockx on &
command -v nm-applet      >/dev/null 2>&1 && nm-applet &
command -v blueman-applet >/dev/null 2>&1 && blueman-applet &
command -v cbatticon      >/dev/null 2>&1 && cbatticon &
command -v pasystray      >/dev/null 2>&1 && pasystray &

command -v fbsetbg        >/dev/null 2>&1 && fbsetbg -l &

xterm -geometry 120x55+0+0 &

exec /usr/bin/fluxbox
EOF

  chmod +x "${USER_HOME}/.fluxbox/startup"

  # .xinitrc
  if [[ ! -f "${USER_HOME}/.xinitrc" ]]; then
    cat > "${USER_HOME}/.xinitrc" <<'EOF'
#!/bin/sh
exec startfluxbox
EOF
    chmod +x "${USER_HOME}/.xinitrc"
  fi

  chown -R "${USER_NAME}:${USER_NAME}" \
    "${USER_HOME}/.fluxbox" \
    "${USER_HOME}/.xinitrc"
}

postrun_summary() {
  echo
  echo "=================================================================="
  echo "[*] Post-run verification"
  echo "=================================================================="
  echo
  echo "[1/7] Cinnamon packages installed (should be none if cleanup ran):"
  # Use if/else to avoid grep failure triggering set -e
  if dpkg -l | awk '{print $2}' | grep -qE '^cinnamon($|-)|^task-cinnamon-desktop$|^cinnamon-desktop-environment$'; then
    echo "  [!] Cinnamon packages still present:"
    dpkg -l | awk '{print $2}' | grep -E '^cinnamon($|-)|^task-cinnamon-desktop$|^cinnamon-desktop-environment$' || true
  else
    echo "  [✓] none"
  fi
  echo
  echo "[2/7] APT policy for cinnamon (expect Pin-Priority -1 if guardrails enabled):"
  apt-cache policy cinnamon 2>/dev/null || echo "  (package not visible in current cache)"
  echo
  echo "[3/7] APT guardrail files:"
  ls -l /etc/apt/apt.conf.d/99lean-desktop /etc/apt/preferences.d/no-cinnamon.pref 2>/dev/null || \
    echo "  (guardrail files not present)"
  echo
  echo "[4/7] CUPS packages:"
  dpkg -l | awk '{print $2}' | grep -E '^cups$|^cups-client$' || echo "  (cups not installed)"
  echo
  echo "[5/7] Browser packages:"
  dpkg -l | awk '{print $2}' | grep -E '^chromium$|^firefox-esr$' || echo "  (no Debian-repo browser installed)"
  echo
  echo "[6/7] PipeWire control tool (wpctl):"
  if command -v wpctl >/dev/null 2>&1; then
    echo "  [✓] found at: $(command -v wpctl)"
  else
    echo "  [!] wpctl not found (likely using ALSA-only or no audio component)"
  fi
  echo
  echo "[7/7] wpctl status (may be minimal pre-login):"
  if command -v wpctl >/dev/null 2>&1 && [[ -n "${USER_NAME:-}" ]]; then
    sudo -u "${USER_NAME}" wpctl status 2>/dev/null || echo "  (no active user PipeWire session yet)"
  else
    echo "  (skipped)"
  fi
  echo
  echo "[✓] Setup complete."
  echo
  echo "Next steps:"
  if [[ -n "${USER_N
AME:-}" ]]; then
    echo "  - Log in as ${USER_NAME}"
  fi
  echo "  - If not using a display manager, run: startx"
  echo "  - Use nm-applet to join Wi-Fi (if installed)"
  echo
  echo "Guardrails (if enabled):"
  echo "  - /etc/apt/preferences.d/no-cinnamon.pref"
  echo "  - /etc/apt/apt.conf.d/99lean-desktop"
  echo
  echo "To re-enable Recommends/Suggests globally:"
  echo "  sudo rm /etc/apt/apt.conf.d/99lean-desktop"
  echo
  echo "To remove the Cinnamon block:"
  echo "  sudo rm /etc/apt/preferences.d/no-cinnamon.pref"
  echo "=================================================================="
}

###############################################################################
# Data-driven component definitions
#
# Format of each entry:
#   "id|Description|packages|hook_function|default_profiles"
#
#   id               : short token, used in flags (--no-id, --enable-id)
#   Description      : human-readable description shown in prompts/plan
#   packages         : space-separated apt package names (or empty)
#   hook_function    : shell function name to run after packages (or empty)
#   default_profiles : comma-separated profiles that enable this by default:
#                        workstation,full,minimal,guardrails
###############################################################################

COMPONENT_SPECS=(
  "cinnamon-clean|Pre-clean Cinnamon packages||pre_clean_cinnamon|workstation,full,minimal,guardrails"
  "guardrails|APT guardrails (no Cinnamon, no Recommends/Suggests)||setup_guardrails|workstation,full,minimal,guardrails"
  "x|Xorg + Fluxbox core|xorg xinit x11-xserver-utils xterm fluxbox fonts-dejavu-core xbacklight||workstation,full,minimal"
  "network|NetworkManager and NM-friendly config|network-manager network-manager-gnome|network_post_install|workstation,full,minimal"
  "utils|Thunar & desktop utilities|thunar thunar-volman arandr numlockx||workstation,full,minimal"
  "bt-batt|Bluetooth & battery tray|blueman cbatticon||workstation,full"
  "audio|Audio (PipeWire stack)|pipewire-audio pipewire-pulse wireplumber pavucontrol pasystray||workstation,full"
  "audio-alsa|Audio (ALSA stack)|alsa-utils alsa-tools alsa-oss||"
  "printing|Printing stack (CUPS)|cups cups-client system-config-printer||workstation,full"
  "browsers|Browsers (Chromium & Firefox ESR)|chromium firefox-esr||workstation,full"
  "extras|Extras (evince, vlc, libreoffice, etc.)|evince vlc libreoffice xtrlock xfburn simple-scan calibre||full"
  "config|Fluxbox user config skeleton (per-user)||setup_fluxbox_config|workstation,full,minimal"
  "dev-tools|Developer tools (git, vim, tmux, htop, build-essential)|git vim tmux htop build-essential curl||workstation,full"
  "media-graphics|Graphics tools (GIMP, Inkscape)|gimp inkscape||full"
  "media-audio|Audio editor tools (Audacity)|audacity||full"
  "dosbox|Retro DOSBox gaming (DOSBox Staging)|dosbox-staging||full"
  "casual-games|Lightweight games (ScummVM, Xboard, XMahjongg, XGalaga)|scummvm xboard gnuchess xmahjongg xgalaga||full"
)

# Arrays to hold parsed component data
declare -a COMPONENT_IDS
declare -A COMP_DESC COMP_PKGS COMP_HOOK COMP_DEFAULTS COMP_ENABLED

for spec in "${COMPONENT_SPECS[@]}"; do
  IFS='|' read -r id desc pkgs hook defaults <<<"$spec"
  COMPONENT_IDS+=("$id")
  COMP_DESC["$id"]="$desc"
  COMP_PKGS["$id"]="$pkgs"
  COMP_HOOK["$id"]="$hook"
  COMP_DEFAULTS["$id"]="$defaults"
done

###############################################################################
# Profiles & config
###############################################################################

INTERACTIVE="false"
DRY_RUN="false"
PROFILE="workstation"

apply_profile() {
  local id defaults
  for id in "${COMPONENT_IDS[@]}"; do
    defaults="${COMP_DEFAULTS[$id]}"
    if [[ ",${defaults}," == *",${PROFILE},"* ]]; then
      COMP_ENABLED["$id"]="true"
    else
      COMP_ENABLED["$id"]="false"
    fi
  done
}

load_config() {
  local base_user base_home cfg_path line key value val_lc

  base_user="${SUDO_USER:-root}"
  base_home="$(eval echo "~${base_user}")"
  cfg_path="${base_home}/.config/debian-fluxbox-setup.conf"

  if [[ ! -r "${cfg_path}" ]]; then
    return 0
  fi

  echo "[*] Loading config from ${cfg_path}"

  while IFS= read -r line || [[ -n "$line" ]]; do
    line="${line%%#*}"
    line="${line#"${line%%[![:space:]]*}"}"
    line="${line%"${line##*[![:space:]]}"}"
    [[ -z "$line" ]] && continue
    [[ "$line" != *=* ]] && continue

    key="${line%%=*}"
    value="${line#*=}"
    key="${key%"${key##*[![:space:]]}"}"
    key="${key#"${key%%[![:space:]]*}"}"
    value="${value%"${value##*[![:space:]]}"}"
    value="${value#"${value%%[![:space:]]}"}"

    case "$key" in
      PROFILE)
        PROFILE="$value"
        apply_profile
        ;;
      *)
        if [[ -n "${COMP_DESC[$key]+_}" ]]; then
          val_lc="$(printf '%s' "$value" | tr 'A-Z' 'a-z')"
          case "$val_lc" in
            true|1|yes|on)  COMP_ENABLED["$key"]="true" ;;
            false|0|no|off) COMP_ENABLED["$key"]="false" ;;
            *)
              echo "[*] Ignoring invalid boolean for ${key} in config: ${value}"
              ;;
          esac
        else
          echo "[*] Ignoring unknown config key: $key"
        fi
        ;;
    esac
  done < "${cfg_path}"
}

# Initialize from default profile, then config
apply_profile
load_config

###############################################################################
# CLI parsing
###############################################################################

USER_NAME="${SUDO_USER:-}"

while [[ $# -gt 0 ]]; do
  case "$1" in
    -u|--user)
      if [[ $# -lt 2 ]]; then
        echo "Error: --user requires an argument" >&2
        usage
        exit 1
      fi
      USER_NAME="$2"
      shift 2
      ;;
    -i|--interactive)
      INTERACTIVE="true"
      shift
      ;;
    -p|--profile)
      if [[ $# -lt 2 ]]; then
        echo "Error: --profile requires an argument" >&2
        usage
        exit 1
      fi
      PROFILE="$2"
      apply_profile
      shift 2
      ;;
    --profile=*)
      PROFILE="${1#*=}"
      apply_profile
      shift
      ;;
    --alsa|--alsa-audio)
      # Switch audio backend: disable PipeWire audio, enable ALSA audio.
      if [[ -n "${COMP_DESC[audio]+_}" ]]; then
        COMP_ENABLED["audio"]="false"
      fi
      if [[ -n "${COMP_DESC[audio-alsa]+_}" ]]; then
        COMP_ENABLED["audio-alsa"]="true"
      fi
      shift
      ;;
    --dry-run)
      DRY_RUN="true"
      shift
      ;;
    --no-*)
      {
        id="${1#--no-}"
        if [[ -n "${COMP_DESC[$id]+_}" ]]; then
          COMP_ENABLED["$id"]="false"
          shift
        else
          echo "Unknown component in flag: $1" >&2
          usage
          exit 1
        fi
      }
      ;;
    --enable-*|--with-*)
      {
        id="${1#--enable-}"
        id="${id#--with-}"
        if [[ -n "${COMP_DESC[$id]+_}" ]]; then
          COMP_ENABLED["$id"]="true"
          shift
        else
          echo "Unknown component in flag: $1" >&2
          usage
          exit 1
        fi
      }
      ;;
    --extras)
      if [[ -n "${COMP_DESC[extras]+_}" ]]; then
        COMP_ENABLED["extras"]="true"
      fi
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    --)
      shift
      break
      ;;
    *)
      echo "Unknown option: $1" >&2
      usage
      exit 1
      ;;
  esac
done

###############################################################################
# Interactive wizard
###############################################################################

ask_component() {
  local id="$1"
  local current="${COMP_ENABLED[$id]:-false}"
  local desc="${COMP_DESC[$id]}"
  local def_str reply

  if [[ "$current" == "true" ]]; then
    def_str="Y/n"
  else
    def_str="y/N"
  fi

  while true; do
    read -r -p "  - ${desc}? [$def_str] " reply || { echo; echo "Aborted."; exit 1; }
    reply="${reply:-}"
    if [[ -z "$reply" ]]; then
      break
    fi
    case "$reply" in
      [Yy]*) COMP_ENABLED["$id"]="true";  break ;;
      [Nn]*) COMP_ENABLED["$id"]="false"; break ;;
      *)     echo "Please answer y or n." ;;
    esac
  done
}

if [[ "${INTERACTIVE}" == "true" ]]; then
  echo "=== Debian + Fluxbox setup interactive mode ==="
  echo

  if [[ -z "${USER_NAME}" ]]; then
    read -r -p "Target username (must already exist, or leave blank for system-only): " USER_NAME
  else
    tmp_user=""
    read -r -p "Target username [${USER_NAME}] (blank to keep): " tmp_user || true
    if [[ -n "${tmp_user:-}" ]]; then
      USER_NAME="${tmp_user}"
    fi
  fi

  echo
  echo "Profile base (current: ${PROFILE}):"
  echo "  workstation, full, minimal, guardrails"
  tmp_profile=""
  read -r -p "Profile name (or blank to keep '${PROFILE}'): " tmp_profile || true
  if [[ -n "${tmp_profile:-}" ]]; then
    PROFILE="${tmp_profile}"
    apply_profile
  fi

  echo
  echo "Toggle components (Y = include, n = drop):"
  for id in "${COMPONENT_IDS[@]}"; do
    ask_component "$id"
  done
  echo
fi

###############################################################################
# Pre-flight checks and plan
###############################################################################

USER_HOME=""
if [[ -n "${USER_NAME:-}" ]]; then
  if ! id "${USER_NAME}" >/dev/null 2>&1; then
    echo "User '${USER_NAME}' does not exist." >&2
    exit 1
  fi
  USER_HOME="$(eval echo "~${USER_NAME}")"
fi

# Root check (skip for dry-run so you can inspect plans as non-root).
if [[ "${DRY_RUN}" != "true" && "$(id -u)" -ne 0 ]]; then
  echo "Please run as root (use sudo)." >&2
  exit 1
fi

# Config component needs a user when actually writing files.
if [[ "${DRY_RUN}" != "true" && "${COMP_ENABLED[config]:-false}" == "true" && -z "${USER_NAME:-}" ]]; then
  echo "Error: 'config' component enabled but no target username was provided." >&2
  echo "Specify --user USER or disable with --no-config or use guardrails-only profile." >&2
  exit 1
fi

os_hint

echo "=================================================================="
echo "Debian + Fluxbox setup - plan"
echo "  Target user:   ${USER_NAME:-}"
echo "  Profile:       ${PROFILE}"
echo "  Dry-run mode:  ${DRY_RUN}"
echo
echo "Components:"
for id in "${COMPONENT_IDS[@]}"; do
  printf "  - %-12s : %s [%s]\n" "$id" "${COMP_DESC[$id]}" "${COMP_ENABLED[$id]:-false}"
done
echo "=================================================================="
echo

# Dry-run: show high-level steps and exit before touching the system.
if [[ "${DRY_RUN}" == "true" ]]; then
  echo "Dry-run mode: no changes will be made."
  echo "Steps that WOULD be executed:"
  echo "  - apt-get update"
  for id in "${COMPONENT_IDS[@]}"; do
    if [[ "${COMP_ENABLED[$id]:-false}" == "true" ]]; then
      pkgs="${COMP_PKGS[$id]}"
      hook="${COMP_HOOK[$id]}"
      echo "  - ${COMP_DESC[$id]}"
      if [[ -n "$pkgs" ]]; then
        echo "      packages: $pkgs"
      fi
      if [[ -n "$hook" ]]; then
        echo "      hook: $hook"
      fi
    fi
  done
  echo
  echo "No packages will be installed or removed, and no files will be written in dry-run mode."
  exit 0
fi

###############################################################################
# Component runner
###############################################################################

run_component() {
  local id="$1"
  local pkgs="${COMP_PKGS[$id]}"
  local hook="${COMP_HOOK[$id]}"

  if [[ "${COMP_ENABLED[$id]:-false}" != "true" ]]; then
    echo "[*] Skipping ${COMP_DESC[$id]}."
    return 0
  fi

  if [[ -n "$pkgs" ]]; then
    echo "[*] Installing packages for ${COMP_DESC[$id]}..."
    # shellcheck disable=SC2086
    apt-get install -y $pkgs
  fi

  if [[ -n "$hook" ]]; then
    "$hook"
  fi
}

###############################################################################
# Execution
###############################################################################

apt_update

for id in "${COMPONENT_IDS[@]}"; do
  run_component "$id"
done

postrun_summary

Additional software (optional, still fits the Fluxbox ethos)

If you want parity with the older “daily driver” list, these still pair nicely with a lightweight setup:

  • Document/PDF: evince
  • Media: vlc
  • Office: libreoffice
  • Locking: xtrlock
  • Optical: xfburn (if you still burn discs)
  • Scanning: simple-scan (if you need it)
  • E-books: calibre

Why I’m comfortable with these guardrails

  • Testing changes quickly. The pin ensures Cinnamon can’t be reintroduced via drift.
  • Lean APT defaults reduce surprises. Install “nice-to-haves” explicitly, when you want them.
  • You can disable this any time. Removing two small files restores default behavior.

Quick troubleshooting

  • Wi-Fi not showing up? Confirm your interfaces aren’t declared in /etc/network/interfaces beyond loopback and that NetworkManager is enabled.
  • wpctl keys not working on first boot? Log into Fluxbox once so your user PipeWire session is active; then test again.
  • Printing not detected? Install model-specific drivers if needed and confirm CUPS is running.

No comments:

Post a Comment