diff --git a/README.md b/README.md index b5337a9..f07d0f8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,135 @@ # Fedora Unified Config -The goal for this repo is to create a unified configuration for Fedora 44 and newer. \ No newline at end of file +Unified configuration for **bare/minimal Fedora 44 and newer**. Pick a desktop +environment and optionally a tiling window manager; the script wires up repos, +packages, the greeter and the session entry. + +This repo is the Fedora-only successor to `starburst`. Everything multi-distro +(Arch, Debian, Ubuntu branches, OS guessing, per-distro package helpers) is gone +on purpose. + +## Usage + +```bash +git clone https://git.ku-it.org/yuri/fedora.git +cd fedora +./setup.sh # interactive: choose DE, then TWM +./setup.sh --debug # same, but pause after each section +``` + +Non-interactive: + +```bash +./setup.sh --yes --de=none --twm=niri +./setup.sh --yes --de=gnome --twm=none +``` + +Run as your **normal user**, not root. `sudo` is used where needed. + +## Layout + +``` +setup.sh entrypoint: args, preconditions, base setup, menus, dispatch +lib/common.sh colours, logging, dnf helpers, rpmfusion/terra, nerd fonts +lib/session.sh display-manager detection/switching, session entries +modules/de/{gnome,plasma,xfce}.sh +modules/twm/{niri,hyprland,mango,chadwm}.sh +``` + +Modules are **separate processes**, not sourced into `setup.sh`. That means a +module's own `set -e` cannot leak into the caller, and every module can be run +on its own for testing: + +```bash +DE=none TWM=niri DEBUG_MODE=false bash modules/twm/niri.sh +``` + +Each module pulls in `lib/` by relative path, so it works both ways. + +## Selection matrix + +| DE | TWM | Result | +|---|---|---| +| any | any | DE provides the session; TWM is added alongside it | +| `none` | any | TWM-only machine, SDDM installed as the greeter | +| any | `none` | plain desktop, no tiling WM | +| `none` | `none` | **refused** — that is not a graphical system | + +### Desktop environments + +| DE | dnf group (tried in order) | native greeter | +|---|---|---| +| `gnome` | `gnome-desktop`, `gnome-desktop-environment`, `workstation-product-environment` | gdm | +| `plasma` | `kde-desktop`, `kde-desktop-environment` | sddm | +| `xfce` | `xfce-desktop`, `xfce-desktop-environment` | lightdm | + +### Tiling window managers + +| TWM | Source | Session type | Notes | +|---|---|---|---| +| `niri` | Fedora repos + Terra (for `noctalia`) | Wayland | Ships its own session entry | +| `hyprland` | Fedora repos | Wayland | Ships its own session entry | +| `mango` | **Terra repo only** | Wayland | Session entry written if the package lacks one | +| `chadwm` | **built from source** | **X11** | dwm fork; not packaged anywhere for Fedora | + +`chadwm` is the only X11 entry in the menu. The other three are Wayland +compositors — do not expect the same config story from it. + +## Display manager policy + +* A **DE module** leaves the DE's own greeter alone (GNOME→gdm, Plasma→sddm, + XFCE→lightdm). It never fights a desktop over its own login screen. +* A **TWM module** enforces **SDDM**, because it is the only greeter that lists + Wayland compositors *and* X11 sessions in one menu. +* Either way `graphical.target` becomes the default. + +## Design rules + +1. **Fail loudly, early.** `set -e` is on unless `--debug`. Unsupported distro, + release below 44, `DE=none` + `TWM=none`, and a missing module all abort with + an explanation instead of half-installing. +2. **Be idempotent.** Re-running must be safe. Repos are checked with `rpm -q` + before being added, the Nerd Font is skipped if already present, and session + entries are not overwritten. +3. **Prefer official repos, then Terra, then a source build.** That order is why + only `mango` and `chadwm` need special handling. +4. **No silent optional failures.** `install_optional` warns and continues for + nice-to-haves; everything load-bearing uses `install_packages` and aborts. +5. **Guard anything environment-dependent.** `clear` needs `|| true` (no `TERM` + under cron/pipes), `os-release` fields are defaulted, and + `/usr/share/xsessions` is created before writing into it. + +## Testing + +The script was verified on a non-Fedora host with a stateful stub harness +(fake `sudo`/`dnf`/`rpm`/`systemctl`/`git`/`make`/`curl`, a fake `os-release`, +and sandboxed `/usr` + `$HOME`). Covered: every DE×(TWM|none) combination, the +refusal paths, `--help`/bad args, interactive menus, `--debug` abort, and +re-running for idempotency. + +`OS_RELEASE_FILE` is overridable purely so the preconditions can be tested: + +```bash +OS_RELEASE_FILE=/tmp/fake-os-release ./setup.sh --yes --de=xfce --twm=chadwm +``` + +## Known limitations + +* `noctalia` and `mangowc` come from **Terra**, a third-party repo. The + package layout there is not guaranteed across Fedora releases; `mango` aborts + with build-from-source instructions if the package is missing. +* Package *names* are not verified by installation in this repo's test harness — + only control flow is. First real run on a fresh F44 box is the actual proof. +* `gnome-tweaks`/`gnome-extensions-app`, `wofi`, `waybar`, `hyprlock` and friends + are installed with `install_optional`, so a rename will warn rather than fail. + +## Roadmap + +- [ ] Profile files (`--profile=laptop`) instead of individual flags +- [ ] Optional dotfiles layer (noctalia/hyprland/niri configs) +- [ ] `--dry-run` that prints the dnf transaction without applying it +- [ ] Pre-flight report: what is already installed, what would change + +## License + +See `LICENSE`. diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 0000000..22344c1 --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,201 @@ +# shellcheck shell=bash +#------------------------------------------ +# lib/common.sh — shared helpers +# +# Sourced by setup.sh and by every module. Modules source it directly, so a +# module can be run standalone for testing: +# DE=none TWM=niri bash modules/twm/niri.sh +#------------------------------------------ + +# Guard against double-sourcing (setup.sh and a module can both pull it in) +if [[ -n "${_SB_COMMON_LOADED:-}" ]]; then + return 0 +fi +_SB_COMMON_LOADED=1 + +#------------------------------------------ +# Colors (printf-safe, no tput) +#------------------------------------------ +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +PURPLE='\033[0;35m' +BLUE='\033[0;34m' +GRAY='\033[0;37m' +BOLD='\033[1m' +RESET='\033[0m' + +# Disable colors if output is not a terminal +if [ ! -t 1 ]; then + RED='' GREEN='' YELLOW='' CYAN='' PURPLE='' BLUE='' GRAY='' BOLD='' RESET='' +fi + +color_yellow() { printf "%b%s%b\n" "$YELLOW" "$*" "$RESET"; } +color_cyan() { printf "%b%s%b\n" "$CYAN" "$*" "$RESET"; } +color_red() { printf "%b%s%b\n" "$RED" "$*" "$RESET"; } +color_green() { printf "%b%s%b\n" "$GREEN" "$*" "$RESET"; } +color_gray() { printf "%b%s%b\n" "$GRAY" "$*" "$RESET"; } + +#------------------------------------------ +# Log levels +#------------------------------------------ +info() { color_cyan " $*"; } +ok() { color_green " $*"; } +warn() { color_yellow " $*"; } +err() { color_red " $*" >&2; } +step() { printf "\n"; color_yellow "$*"; } +rule() { printf "%s\n" "----------------------------------------------------------------"; } + +#------------------------------------------ +# Debug pausing +#------------------------------------------ +is_debug() { [[ "${DEBUG_MODE:-false}" == "true" ]]; } + +pause_if_debug() { + if is_debug; then + printf "\n" + color_yellow "----------------------------------------------------------------" + read -rp "Section complete. Press [Enter] to continue, or [q] to abort: " debug_choice + if [[ "${debug_choice,,}" == "q" ]]; then + color_red "Script aborted by user." + exit 1 + fi + color_yellow "----------------------------------------------------------------" + printf "\n" + fi +} + +#------------------------------------------ +# Package helpers (dnf5) +#------------------------------------------ +install_packages() { + # -y : no prompts + # weak deps on : keep default behaviour, stated explicitly so it is greppable + sudo dnf install -y --setopt=install_weak_deps=True "$@" +} + +# Same, but a missing/renamed package warns instead of aborting the whole run. +# Use for anything that is a nice-to-have rather than load-bearing. +install_optional() { + sudo dnf install -y --setopt=install_weak_deps=True "$@" \ + || warn "Optional packages not installed: $*" +} + +pkg_installed() { + local p + for p in "$@"; do + rpm -q "$p" &>/dev/null || return 1 + done + return 0 +} + +remove_packages() { + local packages_to_remove=() + for pkg in "$@"; do + if pkg_installed "$pkg"; then + packages_to_remove+=("$pkg") + else + color_gray " Package '$pkg' not installed. Skipping removal." + fi + done + + if [ ${#packages_to_remove[@]} -gt 0 ]; then + info "Removing packages: ${packages_to_remove[*]}" + sudo dnf remove -y "${packages_to_remove[@]}" + fi +} + +# dnf comps group. Fail loudly with the real group list rather than letting +# dnf's generic "no match" scroll past. +install_group() { + local group="$1" + if ! dnf group info "$group" >/dev/null 2>&1; then + err "No such dnf group: @$group" + err "Available groups on this system:" + dnf group list --hidden 2>/dev/null | sed 's/^/ /' >&2 || true + return 1 + fi + sudo dnf group install -y "$group" +} + +# Try each candidate group/environment in turn; first one that exists wins. +install_group_any() { + local g + for g in "$@"; do + if dnf group info "$g" >/dev/null 2>&1; then + info "Using dnf group: @$g" + sudo dnf group install -y "$g" + return 0 + fi + done + err "None of these dnf groups exist: $*" + dnf group list --hidden 2>/dev/null | sed 's/^/ /' >&2 || true + return 1 +} + +#------------------------------------------ +# Third-party repos +#------------------------------------------ +add_terra_repo() { + if pkg_installed terra-release; then + ok "Terra repository already installed." + return 0 + fi + info "Adding Terra repository (Fyra Labs)..." + # Terra's own instructions use --nogpgcheck for the bootstrap package; + # once terra-release is in place the repo itself is signature-checked. + sudo dnf install -y --nogpgcheck \ + --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' \ + terra-release +} + +add_rpmfusion_repo() { + local rel + rel=$(rpm -E %fedora) + if pkg_installed rpmfusion-free-release rpmfusion-nonfree-release; then + ok "RPM Fusion already installed." + return 0 + fi + info "Enabling RPM Fusion (free + nonfree) for Fedora ${rel}..." + sudo dnf install -y \ + "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-${rel}.noarch.rpm" \ + "https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-${rel}.noarch.rpm" +} + +#------------------------------------------ +# Fonts +#------------------------------------------ +# Nerd Fonts are not in the Fedora repos, so pull the release zip from GitHub +# into the user's font dir. Idempotent: skipped if the family is already there. +install_nerd_font() { + local family="${1:-JetBrainsMono}" + local dest="${HOME}/.local/share/fonts/NerdFonts-${family}" + local url="https://github.com/ryanoasis/nerd-fonts/releases/latest/download/${family}.zip" + + if [[ -d "$dest" ]] && compgen -G "$dest/*.ttf" >/dev/null; then + ok "Nerd Font ${family} already installed." + return 0 + fi + + info "Installing Nerd Font ${family}..." + local tmp + tmp=$(mktemp -d) + + if ! curl -fsSL -o "$tmp/${family}.zip" "$url"; then + warn "Could not download ${family} Nerd Font. Skipping." + rm -rf "$tmp" + return 0 + fi + + if ! command -v unzip >/dev/null 2>&1; then + install_packages unzip + fi + + mkdir -p "$dest" + unzip -qo "$tmp/${family}.zip" -d "$dest" + rm -rf "$tmp" + + fc-cache -f "$dest" >/dev/null 2>&1 || true + ok "Nerd Font ${family} installed to $dest" +} diff --git a/lib/session.sh b/lib/session.sh new file mode 100644 index 0000000..090e0f3 --- /dev/null +++ b/lib/session.sh @@ -0,0 +1,91 @@ +# shellcheck shell=bash +#------------------------------------------ +# lib/session.sh — display manager and graphical session wiring +# +# DM policy: +# * A DE module leaves its native DM alone (GNOME→gdm, Plasma→sddm, XFCE→lightdm). +# * A TWM module enforces SDDM, because SDDM is the only DM that presents both +# Wayland compositors and X11 sessions in one menu. One greeter, no surprises. +#------------------------------------------ + +if [[ -n "${_SB_SESSION_LOADED:-}" ]]; then + return 0 +fi +_SB_SESSION_LOADED=1 + +# Name of the active display manager ("sddm", "gdm", "lightdm"), or "" if none. +detect_display_manager() { + local link=/etc/systemd/system/display-manager.service + if [[ -L "$link" ]]; then + basename "$(readlink "$link")" | sed 's/\.service$//' + else + echo "" + fi +} + +enable_graphical_target() { + sudo systemctl set-default graphical.target +} + +# Make $1 the display manager, disabling whatever is active now. +switch_display_manager() { + local target="$1" + local current + current=$(detect_display_manager) + + if [[ "$current" == "$target" ]]; then + ok "Display manager already $target." + else + if [[ -n "$current" ]]; then + info "Disabling current display manager: $current" + # disable may fail if the unit vanished; that is not fatal + sudo systemctl disable "$current" 2>/dev/null || true + fi + info "Enabling display manager: $target" + sudo systemctl enable "$target" + fi + + enable_graphical_target +} + +# Register a Wayland session so the DM can list it. +# $1 = identifier, $2 = display name, $3 = command +write_wayland_session() { + local id="$1" name="$2" exec_cmd="$3" + local file="/usr/share/wayland-sessions/${id}.desktop" + if [[ -f "$file" ]]; then + ok "Wayland session already registered: $id" + return 0 + fi + info "Registering Wayland session: $id" + sudo mkdir -p "$(dirname "$file")" + printf '[Desktop Entry]\nName=%s\nComment=%s\nExec=%s\nType=Application\nDesktopNames=%s\n' \ + "$name" "$name session" "$exec_cmd" "$id" | sudo tee "$file" >/dev/null + sudo chmod 644 "$file" +} + +# Register an X11 session so the DM can list it. +write_x11_session() { + local id="$1" name="$2" exec_cmd="$3" + local file="/usr/share/xsessions/${id}.desktop" + if [[ -f "$file" ]]; then + ok "X11 session already registered: $id" + return 0 + fi + info "Registering X11 session: $id" + # A bare Fedora has no X session yet, so the directory may not exist. + sudo mkdir -p "$(dirname "$file")" + printf '[Desktop Entry]\nName=%s\nComment=%s\nExec=%s\nType=Application\n' \ + "$name" "$name session" "$exec_cmd" | sudo tee "$file" >/dev/null + sudo chmod 644 "$file" +} + +# Common login-time plumbing every Wayland compositor wants on a bare install. +install_session_prereqs() { + install_packages \ + xdg-desktop-portal \ + polkit \ + pipewire \ + pipewire-pulseaudio \ + wireplumber +} diff --git a/modules/de/gnome.sh b/modules/de/gnome.sh new file mode 100755 index 0000000..e0a6030 --- /dev/null +++ b/modules/de/gnome.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../../lib/common.sh +source "$HERE/../../lib/common.sh" +# shellcheck source=../../lib/session.sh +source "$HERE/../../lib/session.sh" + +if is_debug; then set +e; else set -e; fi + +step "Desktop environment: GNOME" +rule + +if pkg_installed gnome-shell; then + ok "GNOME already installed." +else + info "Installing the GNOME desktop..." + # Group names have shifted across releases; take whichever exists. + install_group_any gnome-desktop gnome-desktop-environment workstation-product-environment + install_optional gnome-tweaks gnome-extensions-app +fi + +# GNOME ships and enables gdm itself. We deliberately leave it alone: a DE module +# never fights the DE over its own greeter. A TWM module switched on top will +# move the system to SDDM instead. +info "Current display manager: $(detect_display_manager)" + +ok "GNOME setup complete." diff --git a/modules/de/plasma.sh b/modules/de/plasma.sh new file mode 100755 index 0000000..6df4ba6 --- /dev/null +++ b/modules/de/plasma.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../../lib/common.sh +source "$HERE/../../lib/common.sh" +# shellcheck source=../../lib/session.sh +source "$HERE/../../lib/session.sh" + +if is_debug; then set +e; else set -e; fi + +step "Desktop environment: KDE Plasma" +rule + +if pkg_installed plasma-workspace; then + ok "Plasma already installed." +else + info "Installing the KDE Plasma desktop..." + install_group_any kde-desktop kde-desktop-environment +fi + +# Plasma's native greeter is SDDM; make sure it is actually the active one. +install_packages sddm +switch_display_manager sddm + +ok "Plasma setup complete." diff --git a/modules/de/xfce.sh b/modules/de/xfce.sh new file mode 100755 index 0000000..d3c7f03 --- /dev/null +++ b/modules/de/xfce.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../../lib/common.sh +source "$HERE/../../lib/common.sh" +# shellcheck source=../../lib/session.sh +source "$HERE/../../lib/session.sh" + +if is_debug; then set +e; else set -e; fi + +step "Desktop environment: XFCE" +rule + +if pkg_installed xfce4-session; then + ok "XFCE already installed." +else + info "Installing the XFCE desktop..." + install_group_any xfce-desktop xfce-desktop-environment + install_optional "@xfce-extra-plugins" +fi + +# XFCE is X11. It needs an X server present even when it is only the session +# provider for chadwm. +install_packages xorg-x11-server-Xorg xorg-x11-xinit + +# XFCE's native greeter is lightdm +if ! pkg_installed lightdm; then + install_packages lightdm +fi +switch_display_manager lightdm + +install_nerd_font JetBrainsMono +install_packages alacritty dconf + +# Point XFCE's terminal preference at alacritty, seeding the config directly so +# it applies on first login rather than needing a session restart. +XFCONF_DIR="$HOME/.config/xfce4/xfconf/xfce-perchannel-xml" +mkdir -p "$XFCONF_DIR" +info "Seeding XFCE session defaults..." +cat > "$XFCONF_DIR/xfce4-session.xml" <<'EOF' + + + + + + +EOF + +ok "XFCE setup complete." diff --git a/modules/twm/chadwm.sh b/modules/twm/chadwm.sh new file mode 100755 index 0000000..338a564 --- /dev/null +++ b/modules/twm/chadwm.sh @@ -0,0 +1,64 @@ +#!/bin/bash +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../../lib/common.sh +source "$HERE/../../lib/common.sh" +# shellcheck source=../../lib/session.sh +source "$HERE/../../lib/session.sh" + +if is_debug; then set +e; else set -e; fi + +step "Tiling window manager: chadwm" +rule + +# chadwm is an X11 dwm fork and is not packaged for Fedora anywhere. It is +# built from source. Note this is the only non-Wayland option in the menu. +info "Installing chadwm build dependencies..." +install_packages \ + gcc make git \ + libX11-devel libXft-devel libXinerama-devel imlib2-devel \ + dash xsetroot feh picom rofi alacritty \ + xorg-x11-server-Xorg xorg-x11-xinit + +CHAD_DIR="$HOME/.config/chadwm" + +if [[ ! -d "$CHAD_DIR/.git" ]]; then + info "Cloning chadwm into $CHAD_DIR..." + git clone --depth 1 https://github.com/siduck/chadwm "$CHAD_DIR" +else + ok "chadwm source already present at $CHAD_DIR" +fi + +if [[ ! -x /usr/local/bin/chadwm ]]; then + info "Building and installing chadwm..." + # make install needs root to write /usr/local/bin + sudo make -C "$CHAD_DIR/chadwm" install +else + ok "chadwm binary already installed." +fi + +# Session wrapper: dwm forks do not do autostart themselves, so set up the +# wallpaper/compositor here instead of leaving the user with an empty desktop. +info "Installing session wrapper /usr/local/bin/chadwm-session..." +sudo tee /usr/local/bin/chadwm-session >/dev/null <<'EOF' +#!/bin/bash +# chadwm session: compositor + wallpaper + status, then hand over to chadwm. +pgrep -x picom >/dev/null || picom --daemon & +if [ -d "$HOME/Pictures" ]; then + feh --bg-fill "$(find "$HOME/Pictures" -maxdepth 1 -type f \ + \( -iname '*.jpg' -o -iname '*.png' \) | head -n1)" 2>/dev/null || true +fi +xsetroot -cursor_name left_ptr 2>/dev/null || true +exec chadwm +EOF +sudo chmod 755 /usr/local/bin/chadwm-session + +write_x11_session chadwm "chadwm" "/usr/local/bin/chadwm-session" + +install_nerd_font JetBrainsMono + +install_packages sddm +switch_display_manager sddm + +ok "chadwm setup complete. Log out and pick chadwm in SDDM." diff --git a/modules/twm/hyprland.sh b/modules/twm/hyprland.sh new file mode 100755 index 0000000..93d09df --- /dev/null +++ b/modules/twm/hyprland.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../../lib/common.sh +source "$HERE/../../lib/common.sh" +# shellcheck source=../../lib/session.sh +source "$HERE/../../lib/session.sh" + +if is_debug; then set +e; else set -e; fi + +step "Tiling window manager: Hyprland" +rule + +info "Installing Hyprland and its ecosystem..." +install_packages hyprland xdg-desktop-portal-hyprland alacritty +install_packages hyprpaper hypridle hyprlock waybar mako +install_optional wofi +install_optional xwayland-satellite + +install_session_prereqs +install_nerd_font JetBrainsMono + +# The Fedora hyprland package ships hyprland.desktop in wayland-sessions. +install_packages sddm +switch_display_manager sddm + +ok "Hyprland setup complete. Log out and pick Hyprland in SDDM." diff --git a/modules/twm/mango.sh b/modules/twm/mango.sh new file mode 100755 index 0000000..eb161be --- /dev/null +++ b/modules/twm/mango.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../../lib/common.sh +source "$HERE/../../lib/common.sh" +# shellcheck source=../../lib/session.sh +source "$HERE/../../lib/session.sh" + +if is_debug; then set +e; else set -e; fi + +step "Tiling window manager: MangoWC" +rule + +# MangoWC is not in the Fedora repos; it is packaged in Terra. +add_terra_repo + +info "Installing MangoWC..." +if ! sudo dnf install -y mangowc; then + err "mangowc is not available from Terra on this release." + err "Alternatives: copr dennemann/MangoWC, or build from source:" + err " https://github.com/GravityShark/mangowc" + exit 1 +fi + +install_packages fuzzel alacritty +install_optional xdg-desktop-portal-wlr +install_optional xwayland-satellite + +install_session_prereqs +install_nerd_font JetBrainsMono + +# MangoWC's session entry is not guaranteed by the package; write one if absent. +write_wayland_session mangowc "MangoWC" "mangowc" + +install_packages sddm +switch_display_manager sddm + +ok "MangoWC setup complete. Log out and pick MangoWC in SDDM." diff --git a/modules/twm/niri.sh b/modules/twm/niri.sh new file mode 100755 index 0000000..449e6d4 --- /dev/null +++ b/modules/twm/niri.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../../lib/common.sh +source "$HERE/../../lib/common.sh" +# shellcheck source=../../lib/session.sh +source "$HERE/../../lib/session.sh" + +if is_debug; then set +e; else set -e; fi + +step "Tiling window manager: Niri" +rule + +# Noctalia (the shell you settled on) lives in the Terra repo. +add_terra_repo + +info "Installing Niri + Noctalia..." +install_packages niri noctalia fuzzel alacritty +# niri is Wayland-only; this lets X11 apps run inside it +install_optional xwayland-satellite +install_optional xdg-desktop-portal-wlr + +install_session_prereqs +install_nerd_font JetBrainsMono + +# niri ships /usr/share/wayland-sessions/niri.desktop itself, so no entry needed. +install_packages sddm +switch_display_manager sddm + +ok "Niri setup complete. Log out and pick Niri in SDDM." diff --git a/setup.sh b/setup.sh old mode 100644 new mode 100755 index a9cf103..64a3276 --- a/setup.sh +++ b/setup.sh @@ -1,30 +1,10 @@ #!/bin/bash +# +# setup.sh — unified Fedora configuration +# Target: bare/minimal Fedora 44 and newer +# set -uo pipefail -#------------------------------------------ -# Argument parsing (must come before anything reads DEBUG_MODE) -#------------------------------------------ -DEBUG_MODE=false -for arg in "$@"; do - case "$arg" in - -d|--debug) DEBUG_MODE=true ;; - -h|--help) - cat <<'EOF' -Usage: ./setup.sh [-d|--debug] - -d, --debug Pause after each section instead of aborting on error - -h, --help Show this help -EOF - exit 0 - ;; - *) echo "Unknown argument: $arg (try --help)" >&2; exit 2 ;; - esac -done -export DEBUG_MODE -# Abort on error unless we are walking through it step by step -if [[ "$DEBUG_MODE" == "true" ]]; then - set +e -else - set -e -fi + #------------------------------------------ # Color helpers (printf-safe, no tput) #------------------------------------------ @@ -37,19 +17,22 @@ BLUE='\033[0;34m' GRAY='\033[0;37m' BOLD='\033[1m' RESET='\033[0m' + # Disable colors if output is not a terminal if [ ! -t 1 ]; then RED='' GREEN='' YELLOW='' CYAN='' PURPLE='' BLUE='' GRAY='' BOLD='' RESET='' fi + color_yellow() { printf "%b%s%b\n" "$YELLOW" "$*" "$RESET"; } color_cyan() { printf "%b%s%b\n" "$CYAN" "$*" "$RESET"; } color_red() { printf "%b%s%b\n" "$RED" "$*" "$RESET"; } color_green() { printf "%b%s%b\n" "$GREEN" "$*" "$RESET"; } color_gray() { printf "%b%s%b\n" "$GRAY" "$*" "$RESET"; } + #------------------------------------------ -# Helper functions +# Defaults + argument parsing +# DEBUG_MODE must be initialised before anything reads it under set -u. #------------------------------------------ -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" pause_if_debug() { if [[ "${DEBUG_MODE:-false}" == "true" ]]; then echo @@ -63,8 +46,10 @@ pause_if_debug() { echo fi } + install_packages() { sudo dnf install -y "$@"; } is_package_installed() { rpm -q "$@" &>/dev/null; } + remove_packages() { local packages_to_remove=() for pkg in "$@"; do @@ -72,44 +57,54 @@ remove_packages() { packages_to_remove+=("$pkg") fi done + if [ ${#packages_to_remove[@]} -gt 0 ]; then color_cyan "Removing packages: ${packages_to_remove[*]}" sudo dnf remove -y "${packages_to_remove[@]}" fi } + + #------------------------------------------ -# 0 STARTING the script +# Preconditions #------------------------------------------ + #------------------------------------------ -# Script Version 1.1 +# Script Version 1.0 #------------------------------------------ -# Wipe the terminal history view cleanly. Guarded: without TERM (cron, piped -# installs, ssh without a tty) `clear` fails and would abort the whole run. -clear 2>/dev/null || true +clear # Wipe the terminal history view cleanly for the main menu execution + if [[ "$DEBUG_MODE" == "true" ]]; then color_red "!!! DEBUG MODE ENABLED !!!" color_red "The script will pause after each major section." echo fi + echo color_yellow "----------------------------------------------------------------" -color_yellow "Script version 1.1" +color_yellow "Script version 1.0" color_yellow "----------------------------------------------------------------" echo + #------------------------------------------ -# OS Detection +# OS gate — Fedora only, 44+ #------------------------------------------ echo color_yellow "----------------------------------------------------------------" color_yellow "Start OS Detection" color_yellow "----------------------------------------------------------------" echo + source /etc/os-release -OS_ID="${ID:-unknown}"; OS_ID="${OS_ID,,}" -OS_LIKE="${ID_LIKE:-}"; OS_LIKE="${OS_LIKE,,}" + +OS_ID="${ID,,}" +OS_LIKE="${ID_LIKE:-}" +OS_LIKE="${OS_LIKE,,}" OS_VERSION="${VERSION_ID:-unknown}" -OS_PRETTY="${PRETTY_NAME:-unknown}" +OS_PRETTY="$PRETTY_NAME" OS="" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + case "$OS_ID" in fedora) OS="fedora" ;; *) @@ -118,6 +113,7 @@ case "$OS_ID" in fi ;; esac + if [[ -n "$OS" ]]; then color_cyan "----------------------------------------------------------------" color_cyan "Detected OS: $OS_PRETTY" @@ -130,72 +126,79 @@ else color_red "----------------------------------------------------------------" exit 1 fi -# Minimum supported release: this repo targets Fedora 44 and newer. -FEDORA_REL=$(rpm -E %fedora) -if [[ "$FEDORA_REL" =~ ^[0-9]+$ ]] && (( FEDORA_REL < 44 )); then - color_red "$OS_PRETTY reports release $FEDORA_REL, but this setup targets Fedora 44+." - color_red "Refusing to continue. Re-run on F44 or newer." - exit 1 -fi + pause_if_debug #------------------------------------------ -# 1 Basic Settings +# 1 Base settings #------------------------------------------ echo color_yellow "----------------------------------------------------------------" color_yellow "1 Basic Settings" color_yellow "----------------------------------------------------------------" echo + #------------------------------------------ # 1.1 Ensure base is ready for installation #------------------------------------------ if ! command -v curl >/dev/null 2>&1; then - color_yellow "curl is not installed. Installing..." + info "curl missing. Installing..." install_packages curl fi -# config-manager lives in dnf5-plugins; make sure it is present before using it -if ! dnf config-manager --help >/dev/null 2>&1; then - color_yellow "dnf config-manager missing. Installing dnf5-plugins..." - install_packages dnf5-plugins -fi + pause_if_debug + #------------------------------------------ # 1.2 Update and Enable Repos + Flatpak #------------------------------------------ color_cyan "Optimizing DNF for speed..." + # 1. Increase parallel downloads (allows downloading multiple packages at once) # 2. Set dnf to set YES as default instead of NO sudo dnf config-manager setopt max_parallel_downloads=20 sudo dnf config-manager setopt defaultyes=True + color_green "DNF optimized. Refreshing metadata..." sudo dnf makecache --refresh + +color_cyan "Enabling RPM Fusion repositories for Fedora $(rpm -E %fedora)..." + # Define the version variable for readability FEDORA_VER=$(rpm -E %fedora) -color_cyan "Enabling RPM Fusion repositories for Fedora ${FEDORA_VER}..." + +color_yellow "Checking for updates and sources..." +sudo dnf check-update || true + # Check if flatpak is installed, if not, install it if ! command -v flatpak &> /dev/null; then color_yellow "Flatpak not found. Installing..." sudo dnf install -y flatpak fi + +color_cyan "Enabling Flathub..." +# Using --if-not-exists is good practice to prevent errors on re-runs +sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo + sudo dnf install -y \ "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-${FEDORA_VER}.noarch.rpm" \ "https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-${FEDORA_VER}.noarch.rpm" -color_cyan "Enabling Flathub (system-wide)..." -# --if-not-exists keeps this idempotent on re-runs -sudo flatpak remote-add --if-not-exists --system flathub https://dl.flathub.org/repo/flathub.flatpakrepo + +color_cyan "Enabling full Flathub support..." +flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo + # Update the system color_gray "Updating the system" -echo +echohttps://git.ku-it.org/terrabeam/starburst.git sudo dnf update -y pause_if_debug #------------------------------------------ -# 2 Tiling Window Manager Selection +# Tiling Window Manager Selection #------------------------------------------ echo color_yellow "----------------------------------------------------------------" -color_yellow "2 Tiling WM Selection" +color_yellow "Tiling WM Selection" color_yellow "----------------------------------------------------------------" echo + while true; do echo color_gray "Select a tiling window manager:" @@ -208,6 +211,7 @@ while true; do if ! read -t 15 -rp "Enter choice [1/2/3/4/x] (default: x): " choice; then choice="x" fi + case "${choice,,}" in 1) TWM="chadwm"; break ;; 2) TWM="hyprland"; break ;; @@ -217,23 +221,43 @@ while true; do *) echo "Invalid option. Please enter 1, 2, 3, 4 or x." ;; esac done + echo color_cyan "----------------------------------------------------------------" color_cyan "Selected Tiling WM: $TWM" color_cyan "----------------------------------------------------------------" -export TWM + pause_if_debug #------------------------------------------ -# 3 Tiling Window Manager installation +# 5. Tiling Window Manager installation #------------------------------------------ +if [[ -z "$TWM" ]]; then + step "4 Tiling window manager" + rule + printf "\n" + echo " 1) Niri (Wayland, scrollable)" + echo " 2) Hyprland (Wayland, dynamic)" + echo " 3) MangoWC (Wayland, dwl-based, Terra repo)" + echo " 4) chadwm (X11, dwm fork, built from source)" + echo " 5) None" + printf "\n" + choice=$(ask_choice "Select tiling window manager [1/2/3/4/5]" "1" 1 2 3 4 5) + case "$choice" in + 1) TWM="niri" ;; + 2) TWM="hyprland" ;; + 3) TWM="mango" ;; + 4) TWM="chadwm" ;; + 5) TWM="none" ;; + esac +fi + case "$TWM" in chadwm|hyprland|mango|niri) color_yellow "Installing $TWM" SCRIPT_NAME="$SCRIPT_DIR/${TWM}.sh" if [[ -f "$SCRIPT_NAME" ]]; then color_cyan "Running $SCRIPT_NAME" - # Run in a subshell: a module's set -e / exit cannot leak into this script - ( source "$SCRIPT_NAME" ) + source "$SCRIPT_NAME" else color_red "Error: $SCRIPT_NAME not found!" exit 1 @@ -243,11 +267,14 @@ case "$TWM" in color_gray "No tiling window manager selected, skipping TWM installation." ;; esac + pause_if_debug + echo color_yellow "----------------------------------------------------------------" color_yellow "End Detection" color_yellow "----------------------------------------------------------------" + color_green "Fedora setup complete." color_red "The installation has finished. A reboot is recommended." # Reboots are dangerous to automate; we use a shorter timeout and default to No @@ -265,4 +292,4 @@ if read -t 10 -rp "Reboot now? [y/N]: " reboot_choice; then else color_yellow "No input detected. Skipping reboot for safety." exit 0 -fi \ No newline at end of file +fi