Adds '@multimedia' as an ALWAYS_PACKAGES entry and teaches the always-loop
to route '@group' lines through install_group, so a group is checked for
existence first and a bad name prints the system's real group list instead
of dnf's 'No match for argument: @multimedia'.
Group install failures warn in red and continue, like every other
always-package line.
modules/user/nvidia.sh detects the card via lspci and installs the branch
that matches it: current (Turing+), 580xx (Volta/Pascal/Maxwell), 470xx
(Kepler), 390xx (Fermi); pre-Fermi is refused with an explanation.
- no prompt for a detected GPU (--nvidia=no is the opt-out, wired in setup.sh)
- Secure Boot gate: with enforcement on, the akmods key is generated and
imported and the module stops BEFORE installing the driver, so nobody gets
the documented unsigned-module black screen; off/setup-mode skips signing
entirely (a MOK import is never processed when enforcement is off)
- bounded wait for the kmod build, so a reboot cannot land mid-build
- dnf mark user on the akmod; VA-API/vulkan offered as an opt-in extra
- never sets nvidia-drm.modeset=1 (RPM Fusion enables KMS; the flag conflicts
with Fedora's simpledrm patch)
- branch chosen from the chip family in the PCI name, not the device-id
ranges, which overlap between families
Tested with a stub harness (38 assertions, 14 scenarios): all four branch
mappings, multi-GPU warning, Optimus, pre-Fermi refusal, SB on/off/setup,
unparseable name fallback, idempotent re-run, build timeout.
YuKu tested gtk3 on niri: broken UI until a manual swap to qt6 fixed
it. Upstream's own changelog: the GTK3 package is 'for test purpose
only' (Lazarus GTK3 widgetset is alpha) and Qt6 supersedes Qt5.
New mapping: gtk3 ONLY for genuine GTK stacks (gnome, xfce); qt6 for
everything else - plasma, hyprland, niri, mangowc, chadwm (Qt6 handles
Wayland and X11 cleanly).
Harness: full DE/TWM matrix passes (7/7); existing qt5 on niri now
swaps to qt6 (not gtk3); a machine already on qt6 (YuKu's laptop
after the manual swap) is a clean no-op.
Two nits from YuKu's laptop paste: the 100-space line wipe left a
visible gap before short summaries (now ANSI EL: \r\033[K, no
residue), and '(0 packages)' printed when nothing new was installed
(idempotent re-run / theme deps all present) — the count parenthetical
is now omitted when zero.
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).