The theme's own install.sh runs dnf for its dependencies and its
output flooded the screen raw (laptop-proven: the last wall of text
left in the run). Now wrapped in ui_quiet_run: live block line with
the current dependency, summary count after, full output in the
setup log, red + tail on failure. Unattended/piped runs keep plain
output. pipefail still suspended around the yes| pipe (inherited at
subshell fork).
Per YuKu: instead of skipping when an older flavor is present, SWAP it
to the environment-appropriate one (dnf swap -y old new). Atomic —
if the swap fails, the existing flavor stays and the run continues
with a warning (the safe outcome). Right flavor already present ->
no-op; fresh machine -> plain install.
Verified: qt5+niri -> swap to gtk3 (your laptop scenario); qt6+hyprland
-> no-op; forced swap failure -> warn + keep qt5; fresh -> gtk3.
Laptop run (YuKu, existing machine) exposed two fresh-install blind
spots:
1. Terra add failed on a machine that already HAD the repo but not a
queryable terra-release RPM — bootstrapping --repofrompath over an
existing repo id collides. New repo_enabled() checks dnf repolist
(the source of truth); add_terra_repo and add_rpmfusion_repo now
skip when the repo is configured OR the release RPM present.
2. PeaZip tailoring tried gtk3 over an existing peazip-qt5: all UI
flavors ship /usr/bin/peazip and file-conflict, transaction died.
Any installed flavor (qt5/qt6/gtk3/gtk2) now wins and the section
skips with the flavor named.
Harness: laptop sim (repo configured, peazip-qt5 present) -> zero
bootstrap calls, PeaZip skipped; fresh machine -> terra bootstraps +
gtk3 installs; re-run -> skip via repolist; peazip-qt6 + hyprland ->
skip; unattended/piped green.
YuKu on the VM: menu installs were not quiet — the ONE place dnf's
full output still scrolled.
Root cause: the per-item install loop read picks with
'while read ... <<< "$sel"' — a herestring REPLACES stdin for its
whole body, so ui_quiet_run's tty gate saw 'piped' inside the loop
and every menu install ran in plain mode. Picks are now read into an
array first (herestring fills ONLY that); installs run in a for-loop
that never touches stdin.
Also silenced the remaining raw output in the menu path:
- Chrome repo wiring -> install_optional (quiet) + setopt silenced,
direct-rpm fallback via ui_quiet_run
- Brave addrepo -> ui_quiet_run
- flatpak installs -> ui_quiet_run; ui_quiet_run now also matches
flatpak's 'Installing...' progress (Unicode ellipsis) for the live
line and count, sanitized to printable ASCII (tr -cd) so a
non-UTF8 console never sees garbage bytes.
Pty-verified: live block lines + green summaries for chrome/gimp/
vscodium picks, flatpak progress only in the log, zero raw dnf or
flatpak text on screen.
Ported from the old fedora-full script:
- SilentSDDM SDDM theme in the tailoring section, gated on sddm
being installed (skipped on gdm setups), idempotent via the
~/git/SilentSDDM checkout (pull --rebase when present), pipefail
suspended around 'yes | ./install.sh' for the SIGPIPE trap.
- fedora-workstation-repositories added in base alongside the other
repos (it ships the google-chrome repo the Browsers option enables).
- Advanced menu: Editors (VSCodium flatpak), Graphics and Multimedia
(gimp/inkscape/obs/lollypop via dnf, spotify via flatpak), Development
Tools (single option: @development-tools wget cmake ninja-build
pkgconf-pkg-config). All names verified on F44.
- install_category_pkgs splits flatpak:<id> entries to Flathub so mixed
picks cannot fail each other's transactions.
Harness: sddm->theme installs, gnome->skipped, vscodium+spotify via
flatpak, lollypop via dnf in the same multi-pick, devtools as one
transaction, all five categories listed, unattended+piped green.
Per YuKu: after the starship install, append
export STARSHIP_CONFIG=$HOME/.config/starship.toml
eval "$(starship init bash)"
to ~/.bashrc — but ONLY when fedora-dots was NOT cloned (on dots
machines .bashrc belongs to the dots). Also gated on starship actually
being present (no dead eval in every shell) and idempotent (grep
guard). Harness: appended once with dots=no, never with dots=yes
(clean-home re-test; the first test was invalid because rm -rf home/*
left the previous .bashrc — the known dotfile-cleanup pitfall), and
not duplicated on re-run.
Section 8, after the advanced menu (summary renumbered to 9):
- PeaZip variant by environment: peazip-common + peazip-qt6 for
plasma/hyprland, peazip-common + peazip-gtk3 otherwise (all verified
on F44; peazip-qt6-10.9.0-1.fc44). Resilient: warns red, continues.
- Starship prompt via the official installer (command -v guarded), then
the tokyo-night preset ONLY when no starship.toml exists yet —
dotfiles checkouts always win over the preset.
- RobotoMono Nerd Font via the existing install_nerd_font helper
(modules keep installing JetBrainsMono; both are small).
- Services: fstrim.timer, chronyd, NetworkManager globally-managed
config (write + restart + 15s settle ONLY when the conf is absent —
no needless NM restarts on re-runs, and no SSH self-drop when the
config already exists), pipewire user units best-effort.
Polkit agent deliberately NOT ported: solved properly by the
polkit-kde user unit (VM-proven), the old polkit-kde-agent-1 line is
superseded.
YuKu's promised screenshot revealed the real section-6 chaos: the live
line 'block + msg + current package' is longer than 80 columns, so
every redraw WRAPPED to a new line instead of overwriting — the screen
filled with duplicate partial progress lines.
The line is now composed once and clamped to $(tput cols) - 2 with an
ASCII '..' tail. 80-col pty run: zero wrapped live lines (the only
>82-char output left is plain-mode dnf, unchanged by design).
YuKu's expanded always-section died at section 6: p7zip was RETIRED
from Fedora 44 (replaced by the official '7zip' package) -> the
compression line's transaction failed -> set -e killed the whole
install after everything before it had already succeeded.
Two changes:
- p7zip -> 7zip in the Compression line (unace/unrar verified fine:
they live in RPM Fusion, which base enables).
- The always-loop now warns in red per failing category line and
continues — same resilience contract as the advanced menu. One
missing package can no longer abort a 30-minute install at the
finish line.
Harness: healthy run installs everything incl. 7zip/mpv/wget; a
deliberately broken compression line produces the red Failed block,
'finished with failures' summary, exit 0, and later categories
(mpv, wget...) still install.
Two bugs read straight out of YuKu's VM screenshot:
1. 'Unknown argument "add" for command "config-manager"' — dnf5 has
no 'add' subcommand, so the Brave repo never landed and
brave-browser had no match. Now uses 'config-manager addrepo
--from-repofile=' per Brave's own Fedora 41+ docs; skips when
/etc/yum.repos.d/brave-browser.repo exists. The .repo carries the
gpgkey, so the rpm --import dance is gone.
2. 'Installation failed: chromium brave-browser' — multi-select put
all picks in ONE dnf transaction, so brave's no-match killed
chromium too. Picks now install PER ITEM: one failure red-warns
with the item's own name and the remaining picks still install.
YuKu: 'slow the flicker a bit (half or one-third speed)'. The 16-colour
blue<->grey pulse now ticks at 150ms (600ms full cycle, calm); the
256-colour terminal breathe keeps its 50ms deep ramp. Per-depth tick
variable; same in the vendored helper.
Rate proven by pty trace: 8 frames in a 1.2s transaction = 150ms
exactly. (Frame-counting via grep -c undercounts — \r overwrites
collapse frames onto one line; count matches, not lines.)
The on/off pulse shifted the whole line by one character during the
dark frames (off-frame printed one space fewer than the block frame).
Per YuKu: keep the block ALWAYS present and alternate its colour
instead — blue (44) <-> dark grey (100, bright black). Layout is
constant, pulse is colour-based, nothing moves but the block itself.
Also: disable_selinux now mkdir -p's /etc/selinux before writing a
fresh config (harness caught a die on a missing parent dir).
YuKu tests on the bare Linux VT (not a terminal emulator): my 16-colour
fallback alternated blue (44) / bright blue (104), which render nearly
identically on VGA consoles -> fused into 'solid blue'. If TERM
misreports 256, 48;5;N codes are ignored outright.
Fixes:
- TERM linux*/console* forces the 16-colour path regardless of tput.
- 16-colour mode now PULSES THE BLOCK ON/OFF (2 frames blue block,
2 frames no block, ~2.5Hz) instead of shade alternation — visible
on any console hardware, no colour fidelity required.
- 256-colour terminals keep the deep 17->45 breathe ramp.
Verified: TERM=linux pty shows block/no-block alternation with zero
48;5 codes; TERM=xterm-256color keeps the 8-shade ramp; unattended +
piped regressions green.
At 20fps the previous narrow ramp (shades 17-33) flicker-fused into an
apparent SOLID block on the VM. Amplitude now spans dark navy (17) to
bright blue (45) in 12 steps — every frame is visibly different, cycle
is 0.6s. Speed unchanged.
YuKu: 'the breathing is super slow'. Frame interval down from 150ms
to 50ms (~7fps -> ~20fps pulse). Same change in the vendored
lib/ui-block.sh helper for consistency.
ui_quiet_run now paints a TTY-safe status BLOCK: a background-coloured
SPACE (never a glyph, no font/locale dependency — the braille lesson).
busy : blue block breathing through a 256-colour ramp (48;5;17..33)
with the live dnf action after it
ok : solid green block + package count
fail : solid RED block (spec: YuKu changed the red X to a red block)
followed by the log tail
Colour-depth aware: 256-colour ramp -> 16-colour blue/bright-blue ->
plain ASCII spinner when the terminal reports no colour. LANG=C pty
run shows zero non-ASCII bytes.
Also vendors lib/ui-block.sh (the standalone helper from the Telegram
session prototype; fail changed to red block) and installs the
ui_block_close EXIT trap — with a return 0, because an EXIT trap's
status becomes the script's exit code (harness caught every-successful-
run exiting 1 through that path).
The alias only existed where it was typed by hand; a fresh VM with
dots cloned had no cfg command. The module now always ensures:
- ~/.bash_aliases holds the cfg alias (created if absent)
- ~/.bashrc sources ~/.bash_aliases (appended if absent)
Runs before the already-installed skip so existing machines get it
too. Idempotent both ways (harness-proven: single alias line, single
source line after two runs; verified working in an interactive shell:
'type cfg' resolves and 'cfg status' runs).
~/.bash_aliases is the right file to TRACK in fedora-dots next: it
will then sync further aliases across machines.
The braille spinner frames and the em-dash separator are multibyte
UTF-8; on the VM session they rendered as changing random letters, not
a spinner. Frames are now the classic | / - \ cycle, the separator is
':', and the two user-facing info lines that carried em-dashes are
ASCII too. Verified under LANG=C LC_ALL=C in a pty: zero non-ASCII
bytes in the entire output.
First sudo call of the session printed its password prompt straight
into the live progress line: 'Installing (optional): gum[sudo]
password for yuri:' (VM screenshot). sudo -v asks once, on a clean
screen, right after the banner; the timestamp refreshes on every later
sudo use, so one prompt covers the whole run.
gum spin rendered static on the real VM during long transactions.
ui_quiet_run now draws its own line: braille spinner frame + the
package dnf is currently working on (from the transaction log), at
150ms ticks. Works without gum (pure bash), gated on interactive
terminal, not on gum.
set -e + pipefail bug found via pty trace: the first tick races the
child's first log line, grep exits 1 (no match) and pipefail turns the
whole substitution nonzero, killing the script silently at the first
frame. || true on the substitution pipeline fixes it.
ui_quiet_run wraps every install_packages/install_optional/group
install/remove and the system update: while gum is available and the
run is interactive, dnf output is hidden behind a spinner, then a
single 'Installing: <pkgs> (N packages)' line. Full output is appended
to $SETUP_DNF_LOG (/tmp/fedora-setup-dnf-<uid>.log); on failure the
last 20 lines print in red so nothing is lost. Unattended and no-gum
runs keep plain full output (logs/cron contract).
Also: fallback ui_choose accepts 'x' to exit (muscle memory from the
old menu), fixed a stray stdout newline leaking into captured answers
(same class as the earlier '\nno' bug), fixed pluralization.
Harness: piped fallback, pty gum run incl. a failing VirtualBox pick
(red failure + log tail, menu survives, subsequent qemu install ok),
unattended regression.
Interactive install gets styled menus, confirms, multi-select and a
spinner around the system update when gum is available (dnf install
gum, installed optionally in the base section); every prompt falls
back to the classic numbered/yn prompts otherwise, and --yes runs
stay 100% non-interactive.
Call sites moved to ui_choose/ui_confirm/ui_choose_multi/ui_run:
DE + TWM menus (defaults preserved: none/none), SELinux + dotfiles
questions, advanced-menu category + multi-select app picker, reboot
prompt. Dead ask_choice/ask_yes_no removed.
Two bugs caught by pty-based harness testing before shipping:
- stdout is a pipe inside $(ui_...), so have_gum must test stdin +
/dev/tty, never -t 1 (gum would never have fired on a real term).
- fallback display output must go to >&2 — a stray newline on stdout
made choice='\nno' and broke every case match.
Fresh-VM timeline explained: on run 1 virt-manager installs AFTER the
niri module's prereq check ran (menu dispatch order), so the group add
was evaluated against a package state that did not include it yet. A
second run 'fixed' it — matching the observed behavior.
- New ensure_libvirt_group runs in the Summary section, seeing the FINAL
package state; menu/module-time check kept as an early pass.
- pkg_installed is AND-semantics (all must be installed); the gate needs
OR. New pkg_installed_any helper; both gates switched to it.
- Also fixed a set -e trap: '$done_any && ok' returned 1 (killing the
script) when no optional tools were picked — replaced with an if.
All four gate cases harness-proven: virt-only, menu-pick, no-virt,
already-in-group.
Fresh-VM repro: user in yuri+wheel with virt-manager installed. Gate was
'pkg_installed libvirt', but virt-manager does NOT require the 'libvirt'
metapkg (verified vs F44 package page: requires libvirt-glib etc., client
side only) — so the usermod never fired.
Gate now: libvirt OR libvirt-daemon OR libvirt-daemon-driver-qemu OR
virt-manager packages, or an existing /usr/sbin/libvirtd binary.
usermod used $USER directly; under sudo $USER can be root. All other
user-scoped calls already use ${SUDO_USER:-$USER} — aligned. No hardcoded
usernames anywhere in the repo (verified by grep; only the dotfiles repo
URL, which is overridable via DOTFILES_REPO).
The error CHANGED on the VM: 'no polkit agent' -> 'access denied by
policy' — the agent unit works; polkit now denies because the user is
not in the libvirt group (fresh run without picking QEMU from the menu,
or no re-login after the pick).
install_session_prereqs now adds the user to the libvirt group whenever
libvirt is installed (idempotent via id -nG check), decoupled from the
advanced-menu choice. Applies after re-login.
Second VM failure: Fedora's polkit-kde .desktop carries
X-systemd-skip=true (and OnlyShowIn on some versions), so niri's
xdg-desktop-autostart.target skips the agent regardless — package
installed, agent dead, same error.
Stop fighting .desktop semantics: install a polkit-agent.service USER
unit (WantedBy=graphical-session.target, which niri/Hyprland/Mango all
bring up) with the Fedora kf6 binary path, pre-linked into
graphical-session.target.wants. Idempotent on both file and symlink.
The VM error persisted with polkit-kde installed: the package's autostart
.desktop carries OnlyShowIn=KDE;, so XDG autostart (which niri honors via
xdg-desktop-autostart.target) skips the agent in a niri session. Package
present, agent never running — exactly the observed behavior.
install_session_prereqs now rewrites the .desktop without the OnlyShowIn
line in place (idempotent: only rewrites when the line is present).
A clean-snapshot run showed qemu/virt-manager installed without choosing
it: PR #4 had embedded the install in the niri module. Removed from there
(zero virt packages in a plain niri run, harness-verified) and moved into
install_category_pkgs Virtualisation: picking qemu/virt-manager there now
also enables libvirtd and adds the user to the libvirt group.
VM run failed: 'No match for argument: polkit-gnome'. polkit-gnome is
orphaned in Fedora (EPEL9 only). polkit-kde 6.6.4 is in F44 and ships its
agent's /etc/xdg/autostart .desktop itself, so the manual symlink wiring
is gone too — installing the package is the whole fix.
virt-manager on niri/Noctalia failed with 'authentication unavailable:
no polkit agent available to authenticate action org.libvirt.unix.manage'
- bare compositors ship no polkit agent, unlike GNOME/KDE.
Two fixes:
- install_session_prereqs installs polkit-gnome and wires its agent
.desktop into /etc/xdg/autostart (paths overridable for testing).
- niri module optionally installs the qemu/libvirt stack, enables
libvirtd, and adds the user to the libvirt group: members manage
qemu:///system with no polkit prompt at all (the real fix; applies
after re-login).
Root cause of the VM failures (both qemu-kvm AND VirtualBox showed 'No
match for argument: <entire list>'): picks were resolved into ONE quoted
string and passed to dnf as a single argument, so dnf searched for a
package literally named 'qemu-kvm libvirt virt-install ...'. Picks are
now word-split into separate package arguments.
Also, a failing install no longer kills the script (set -e abort): the
menu prints a red 'Installation failed' warning, notes the system is
unchanged, and drops back to the menu so another choice can be made.
Proven in harness: VBox failure (no match) followed by successful qemu
install in the same session, exit 0.
Section 6 'Always-installed packages': ALWAYS_PACKAGES array, one line per
category (first: Terminals with ghostty + ghostty-bash-completion). Every
entry installs on every run, no questions. Add a category by adding a line.
Section 7 'Advanced optional tools': menu-driven picker, data-driven —
ADV_CATEGORIES + ADV_<Category> arrays ('id:Label:pkgs...'). Ships
Virtualisation (qemu/kvm stack, VirtualBox) and Browsers (firefox,
chromium, google-chrome, brave; the latter two wire up their repos
automatically). Multi-select with comma-separated picks, invalid picks
warned and skipped, 'x' backs out, menu is skipped under --yes so
unattended runs never hang.
Bugs found by harness testing and fixed before shipping:
- ${!var} on an array name yields only element[0]: Browsers listed one
entry. Now uses a nameref (declare -n).
- var assignment was lost in an edit: unbound-variable crash on the menu.
- --snapper=no was overwritten by the interactive default; explicit flags
now always win.
- On btrfs, snapshots + zstd compression now just happen (no prompt);
--snapper=no remains the explicit opt-out.
- Vendor scripts/snapper-*.sh + config/snapper.actions from
SysGuides/sysguides-snapper-fedora (MIT, upstream 0a28eb4) into
vendor/snapper/ and install from there — no runtime clone of an
external repo, and no dependency on exec bits (fixes the
'./install.sh: Permission denied' failure seen on the test VM).
- grub-btrfs still cloned at runtime from Antynea/grub-btrfs with the
Fedora config applied; everything else is local now.
New modules/user/dotfiles.sh behind a menu question ('Clone personal
dotfiles?', default no) and --dotfiles=yes|no flag.
Does a --bare clone of yuri/fedora-dots into ~/.cfg.git, checks the repo
out into $HOME with status.showUntrackedFiles=no, and handles an empty
repo gracefully (configures the alias, skips checkout). Existing
~/.cfg.git is left untouched (idempotent).
Fix proven in the harness: the module originally used a plain git clone,
which creates a working copy and breaks the git-dir alias — the same
failure YuKu hit manually; --bare is required.
Dispatch runs the dotfiles module before DE/TWM so configs are in place
when sessions are installed.
noctalia 5.3.0 is in the Fedora 44 updates repo (confirmed on
packages.fedoraproject.org and by a fresh-install dnf install noctalia).
The niri module no longer calls add_terra_repo; Terra stays in the base
section and in the mango module, which is the only consumer left
(mangowc). README + comments updated.
New section between base and DE menus:
- 'Disable SELinux permanently?' — default no; writes SELINUX=disabled with a
backup, warns about the security tradeoff.
- When / is btrfs: 'snapper + grub-btrfs + compression?' — default yes. Shows
subvolume list + fstab, appends ,compress=zstd:1 to btrfs fstab entries that
lack a compress= option (idempotent, fstab backed up), then runs the
SysGuides sysguides-snapper-fedora installer (snapper root+home, grub-btrfs,
DNF5 pre/post snapshot actions).
- Flags for unattended runs: --selinux=keep|disabled, --snapper=yes|no.
- Fix found by the harness: the flag initializers sat after the arg loop and
wiped parsed values; --snapper=no was silently ignored. Initializers moved
before the loop.
README + section renumbering updated.
On F44, 'dnf copr enable solopasha/hyprland' fails with 'Chroot not found':
verified via the Copr API, solopasha is rawhide-only right now. lionheartp/
Hyprland (solopasha fork, also ships noctalia packages) and hermitfeather/
hyprland both have fedora-44 chroots.
The module now tries lionheartp, hermitfeather, then solopasha, and aborts
with an actionable message if none has a chroot for this release. README
updated to name the chain, not a single repo.
- Terra repo is added right after RPM Fusion in the base setup (it carries
noctalia + mangowc). Modules keep calling add_terra_repo so they still
work standalone; it is a no-op when terra-release is present.
- Hyprland was retired from the Fedora repos after F42 (dependency churn);
the module could never resolve 'hyprland' on F44. It now enables the
solopasha/hyprland COPR first — the de facto standard for F43/44 — and
treats the accessory packages (hyprpaper/hyprlock/waybar/mako/wofi/
xwayland-satellite) as optional so a name drift warns instead of aborting.
- README updated.
- XFCE module installs/enables sddm instead of lightdm; one greeter across
every session choice, no lightdm left anywhere.
- Interactive menus now default to 'none' for both DE and TWM (press Enter
for no DE / no TWM). none+none is still refused by the existing guard.
- README and session.sh comments updated to match.
The corrections commit replaced the lib/+modules/ entrypoint with the
older single-file version, reintroducing the DEBUG_MODE unbound-variable
crash, the unguarded clear, and TWM dispatch pointing at nonexistent
root-level scripts. lib/ and modules/ were orphaned.
Restores the tested restructured setup.sh + the SC2034 fix in lib/common.sh.
Splits setup.sh into lib/common.sh, lib/session.sh and seven modules
(3 DE, 4 TWM). Targets a bare Fedora 44+ install.
Fixes carried over from the first build:
- DEBUG_MODE was unbound under set -u, aborting the script at the version banner
- mangled 'echohttps://...' line left over from the starburst split
- set -e was missing entirely; --debug flag parsing restored
- unguarded 'clear' aborted the run without TERM
- $PRETTY_NAME unguarded under set -u
- modules were sourced, leaking shell options into the parent
- Flathub added twice (system-wide + per-user)
- no dnf5-plugins check before using 'dnf config-manager'
- no Fedora release gate
- /usr/share/xsessions assumed to exist when writing session entries