Let a machine say what it is for, and give Firefox its face back

Phase 4: the optional application categories, and the Firefox chrome.

Everything Panama installed until now was what every machine gets, which meant a
work laptop acquired emulators and a desktop that wanted Steam had to be told
about it by hand. The interview now offers the categories in
setup/packages/extras/ as a checklist -- gaming, creative, communication,
virtualization -- and nothing is preselected, because a default here installs
applications nobody chose on a machine whose owner answered a question they
thought was about something else.

A category is one file, and a category mixes both package managers because the
applications do: Steam is in RPM Fusion, Slack publishes only a flatpak. So a
bare line is a dnf package and a flatpak: line is a Flathub ID, and one file
holds the whole answer rather than splitting each category across two. The menu
is read from the directory rather than written down, so adding a category is
adding a file. Every name in all four was resolved against the actual
repositories before being written down, and the contract re-resolves them --
the point of admitting applications one at a time is that they stay installable,
and a typo here fails on somebody else's machine, not this one.

Firefox is declared, and its chrome is Edge-Frfox, vendored into config/firefox.
sunhat carried that theme with no license and no attribution; it is MIT, and now
it says so and says whose it is.

It is the only piece of Panama's configuration that does not go to a path this
repository chooses. Firefox owns the profile directory, names it with a random
salt, and does not create one until the browser has been run -- so link-dotfiles
finds or creates a profile and links both halves into it. Both, or neither works:
chrome/ is the CSS and user.js sets the preference that makes Firefox read chrome/
at all, without which the theme is a directory of dead files.

Two assumptions there were wrong, and the contract exists for both. Firefox has
moved to the XDG directories -- the profile root is ~/.config/mozilla/firefox on
this build, not ~/.mozilla/firefox, and writing to the wrong one themes nothing
and says nothing about it. And -CreateProfile turns out to be non-interactive, so
a fresh machine gets the theme on the first install rather than the second. The
contract runs link-dotfiles for real against a throwaway home with no profile in
it and looks at what came out; it was checked by pointing the search at the
legacy path only and watching it fail.

Also: the enrolment/enrollment spellings from the last commit are corrected. This
repository is US-spelled everywhere else -- color 1131 times against colour never
-- and consistency in prose is worth as much as it is in code.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
This commit is contained in:
Gabriel Brown
2026-08-20 21:24:15 -04:00
parent b319d1a5e1
commit 88497826ec
168 changed files with 6293 additions and 32 deletions
+4
View File
@@ -10,6 +10,10 @@ desktop-file-utils
dnf-plugins-core
ffmpeg
firamono-nerd-fonts
# A second engine to check pages against; Helium is the daily driver. Its
# chrome is themed from config/firefox, which link-dotfiles places into the
# profile.
firefox
fuse-libs
# Ghostty and its config in config/dot/ghostty are linked by link-dotfiles.
ghostty
+8
View File
@@ -0,0 +1,8 @@
# Chat and messaging. Every one of these is Flathub only -- none is packaged
# for Fedora, and each publishes its own flatpak.
#
# Thunderbird is deliberately absent: it is already in flatpak-packages as
# org.mozilla.thunderbird_esr, which every machine gets.
flatpak:com.discordapp.Discord
flatpak:com.slack.Slack
flatpak:org.signal.Signal
+7
View File
@@ -0,0 +1,7 @@
# Images, video and audio. All from dnf or RPM Fusion.
gimp
# Video editing, screen capture, and transcoding what comes out of them.
kdenlive
obs-studio
HandBrake
+14
View File
@@ -0,0 +1,14 @@
# Games and the things that run them. A work laptop declines this whole
# category; a desktop wants all of it.
#
# steam is in RPM Fusion nonfree, which install-packages enables before it
# reads this file.
steam
lutris
# Frame timings on an overlay, and the compositor Steam's own session uses.
mangohud
gamescope
# Proton and Wine build management for Steam. Flathub only -- there is no RPM.
flatpak:com.vysp3r.ProtonPlus
+6
View File
@@ -0,0 +1,6 @@
# Other machines, and other distributions, from this one.
#
# podman is not here: the Containers settings page depends on it, so it is a
# declared dependency in development-packages rather than an optional extra.
virt-manager
distrobox
+6 -6
View File
@@ -4,7 +4,7 @@
# NVIDIA driver, the machine owner key that lets it load under Secure Boot,
# Fedora's preinstalled extras, and firmware.
#
# Runs last. MOK enrolment arms a prompt consumed at the next boot and firmware
# Runs last. MOK enrollment arms a prompt consumed at the next boot and firmware
# updates can ask for a reboot, so neither belongs in front of the package work
# or the dotfiles -- a machine that reboots out of this stage has already been
# fully configured.
@@ -66,7 +66,7 @@ fi
# ── Secure Boot ──────────────────────────────────────────────────────────────
#
# akmods signs the modules it builds with a key it generates on installation.
# Under Secure Boot that key means nothing until it is enrolled, and enrolment
# Under Secure Boot that key means nothing until it is enrolled, and enrollment
# is deliberately a thing only somebody at the physical machine can complete:
# the request is queued here, and the next boot shows a blue screen asking for
# the password before it will trust the key.
@@ -83,7 +83,7 @@ if [[ -n "$mok_hash" ]]; then
cert="${PANAMA_MOK_CERT:-/etc/pki/akmods/certs/public_key.der}"
if [[ ! -r "$cert" ]]; then
warn "No akmods certificate at $cert, so there is no key to enrol"
warn "No akmods certificate at $cert, so there is no key to enroll"
elif mokutil --test-key "$cert" 2>/dev/null | grep -q 'already enrolled'; then
log "The akmods key is already enrolled"
else
@@ -91,10 +91,10 @@ if [[ -n "$mok_hash" ]]; then
chmod 600 "$hash_file"
printf '%s\n' "$mok_hash" >"$hash_file"
if sudo mokutil --import "$cert" --hash-file "$hash_file"; then
log "Key enrolment requested"
log "At the next boot, choose 'Enrol MOK' and enter the password you gave the installer"
log "Key enrollment requested"
log "At the next boot, choose 'Enroll MOK' and enter the password you gave the installer"
else
warn "Key enrolment failed; the NVIDIA module will not load until it is enrolled"
warn "Key enrollment failed; the NVIDIA module will not load until it is enrolled"
fi
rm -f "$hash_file"
fi
+47
View File
@@ -180,3 +180,50 @@ if [[ -f "$FLATPAK_FILE" ]]; then
else
log "Package list was not in specified path: $FLATPAK_FILE"
fi
# --- Install the extras that were chosen ------------------------------------
#
# Everything above is what every Panama machine gets. This is what one machine
# asked for: the interview offers the categories in setup/packages/extras/ as a
# checklist and records the chosen names, so a work laptop does not acquire
# emulators and a desktop does not skip Steam.
#
# Absent means none. That is what makes this stage safe to re-run by hand while
# repairing one piece of a machine -- and it means a category is installed only
# by an explicit answer, never by a default that drifted.
#
# A category mixes both package managers, because the applications do: some are
# in Fedora or RPM Fusion and some publish only a flatpak. A bare line is a dnf
# package and a `flatpak:` line is a Flathub ID, so one file per category holds
# the whole answer rather than splitting each category across two.
#
# Neither install is fatal. A category is a set of applications somebody wanted,
# not a dependency of the desktop, and losing the rest of the run because one of
# them was renamed upstream would be the wrong trade.
install_extra_category() {
local file="$1" name
name="$(basename "$file")"
local dnf_packages flatpak_ids
dnf_packages=$(packages_in "$file" | tr ' ' '\n' | grep -v '^flatpak:' | tr "\n" " ")
flatpak_ids=$(packages_in "$file" | tr ' ' '\n' | sed -n 's/^flatpak://p' | tr "\n" " ")
if [[ -n "${dnf_packages// /}" ]]; then
log "Installing $name: $dnf_packages"
sudo dnf install -y $dnf_packages > /dev/null || log "Some $name packages did not install"
fi
if [[ -n "${flatpak_ids// /}" ]]; then
log "Installing $name flatpaks: $flatpak_ids"
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo > /dev/null
sudo flatpak install -y flathub $flatpak_ids > /dev/null || log "Some $name flatpaks did not install"
fi
}
EXTRAS_DIR="$PANAMA_PATH/setup/packages/extras"
for extra in ${PANAMA_EXTRAS:-}; do
if [[ -f "$EXTRAS_DIR/$extra" ]]; then
install_extra_category "$EXTRAS_DIR/$extra"
else
log "No such extras category: $extra"
fi
done
+36 -9
View File
@@ -15,9 +15,8 @@
# somebody else could clone. Re-answering a handful of questions costs less than
# maintaining an answers file that drifts out of date.
#
# This asks only what a stage in this repository actually consumes. The extras
# checklist arrives with the stage that acts on it; a prompt whose answer nothing
# reads is a control that lies.
# This asks only what a stage in this repository actually consumes. A prompt
# whose answer nothing reads is a control that lies.
#
# The hardware questions name what was found rather than asking a person to
# recite their own machine, and they are not asked at all on a machine they
@@ -109,16 +108,16 @@ if [[ -n "$nvidia_card" ]]; then
# Only asked where it does something. On a machine with Secure Boot off,
# akmods' signature is never checked and enrolling a key is ceremony.
if mokutil --sb-state 2>/dev/null | grep -qi 'secureboot enabled'; then
printf 'Secure Boot is on, so the driver must be signed with a key you enrol.\n'
printf 'Secure Boot is on, so the driver must be signed with a key you enroll.\n'
printf 'The next boot will ask for this password on a blue screen.\n'
if yes_no "Enrol a machine owner key?"; then
if yes_no "Enroll a machine owner key?"; then
# Hashed here and only the hash recorded. The password never
# reaches the answers file, the environment, or a command line
# -- mokutil takes a hash file precisely so it does not have to.
while :; do
first="$(gum input --password --header "MOK password")"
if [[ -z "$first" ]]; then
printf 'No password given; skipping enrolment.\n'
printf 'No password given; skipping enrollment.\n'
break
fi
second="$(gum input --password --header "MOK password again")"
@@ -160,6 +159,33 @@ if command -v fwupdmgr >/dev/null 2>&1; then
fi
record PANAMA_FIRMWARE "$firmware"
# ── Applications ─────────────────────────────────────────────────────────────
#
# Everything else in this repository is what every Panama machine gets. This is
# the one question about what this machine is for: a work laptop should not
# acquire emulators and a desktop should not skip Steam.
#
# The categories are read from the directory rather than listed here, so adding
# one is adding a file. Nothing is preselected -- a default here would install
# applications nobody chose, on a machine whose owner answered a question they
# thought was about something else.
heading "Applications"
extras_dir="$(dirname "${BASH_SOURCE[0]}")/../packages/extras"
extras=""
if [[ -d "$extras_dir" ]]; then
mapfile -t categories < <(for file in "$extras_dir"/*; do
[[ -f "$file" ]] && basename "$file"
done)
if (( ${#categories[@]} > 0 )); then
printf 'Optional application categories. Space to select, enter to accept.\n'
extras="$(gum choose --no-limit --header "Extras" "${categories[@]}" | tr '\n' ' ')"
extras="${extras% }"
fi
fi
record PANAMA_EXTRAS "$extras"
# ── Confirm ──────────────────────────────────────────────────────────────────
#
# The last chance to catch a typo before twenty minutes of package work that
@@ -176,9 +202,10 @@ gum style --border rounded --padding "0 1" "$(
printf 'GitHub %s\n' "$([[ "$gh_login" == yes ]] && echo "sign in" || echo "no change")"
printf 'SSH key %s\n' "$([[ "$ssh_key" == yes ]] && echo "generate" || echo "no change")"
printf 'NVIDIA %s\n' "$([[ "$nvidia" == yes ]] && echo "install driver" || echo "no")"
printf 'Secure Boot %s\n' "$([[ -n "$mok_hash" ]] && echo "enrol a key" || echo "no change")"
printf 'Extras %s\n' "$([[ "$debloat" == yes ]] && echo "remove ${installed[*]}" || echo "keep")"
printf 'Firmware %s' "$([[ "$firmware" == yes ]] && echo "update" || echo "no")"
printf 'Secure Boot %s\n' "$([[ -n "$mok_hash" ]] && echo "enroll a key" || echo "no change")"
printf 'Fedora apps %s\n' "$([[ "$debloat" == yes ]] && echo "remove ${installed[*]}" || echo "keep")"
printf 'Firmware %s\n' "$([[ "$firmware" == yes ]] && echo "update" || echo "no")"
printf 'Extras %s' "${extras:-none}"
)"
if ! gum confirm --default=true "Install with these answers?"; then
+64
View File
@@ -288,6 +288,70 @@ if [ -d "$PANAMA_WALLPAPER_DIR" ]; then
done
fi
# The Firefox chrome. Edge-Frfox, vendored in config/firefox -- see the README
# there for what it is and whose it is. Both halves have to land or neither
# works: chrome/ holds the CSS, and user.js sets the preference that makes
# Firefox read chrome/ at all.
#
# Firefox has moved to the XDG directories, so a current build keeps profiles in
# ~/.config/mozilla/firefox and an older one in ~/.mozilla/firefox. Both are
# looked for rather than assumed -- assuming picked the wrong one on the machine
# this was written on.
PANAMA_FIREFOX_DIR="$PANAMA_PATH/config/firefox"
firefox_root=""
find_firefox_root() {
local candidate
for candidate in "${XDG_CONFIG_HOME:-$HOME/.config}/mozilla/firefox" "$HOME/.mozilla/firefox"; do
if [ -f "$candidate/profiles.ini" ]; then
firefox_root="$candidate"
return 0
fi
done
return 1
}
if [ -d "$PANAMA_FIREFOX_DIR" ] && command -v firefox >/dev/null 2>&1; then
echo -e "\n--- Setting up Firefox chrome ---"
# A profile does not exist until Firefox has been run once. -CreateProfile is
# a documented flag that creates one and exits rather than opening a window,
# so a fresh machine gets the theme on the first install instead of the
# second. MOZ_HEADLESS in case a future build decides otherwise.
if ! find_firefox_root; then
log "No Firefox profile yet; creating one"
MOZ_HEADLESS=1 firefox -CreateProfile panama >/dev/null 2>&1 || true
find_firefox_root || true
fi
if [ -z "$firefox_root" ]; then
log "Could not find or create a Firefox profile; skipping the chrome"
else
# Every profile, not just the default. profiles.ini records each one as a
# path that is relative to the root unless it says otherwise.
while read -r profile; do
[ -n "$profile" ] || continue
[ -d "$profile" ] || continue
for piece in chrome user.js; do
target="$profile/$piece"
if [ -L "$target" ]; then
rm "$target"
elif [ -e "$target" ]; then
backup="$PANAMA_OLD/firefox-$(basename "$profile")-$piece"
mv "$target" "$backup"
log "Moved existing $target to $backup"
fi
ln -s "$PANAMA_FIREFOX_DIR/$piece" "$target"
log "Linked $PANAMA_FIREFOX_DIR/$piece → $target"
done
done < <(awk -F= -v root="$firefox_root" '
/^\[/ { relative = 1; next }
/^IsRelative=/ { relative = $2; next }
/^Path=/ { print (relative == 1 ? root "/" $2 : $2) }
' "$firefox_root/profiles.ini")
fi
fi
PANAMA_APPLICATION_DIR="$PANAMA_PATH/config/local/share/applications"
USER_APPLICATION_DIR="$HOME/.local/share/applications"
mkdir -p "$USER_APPLICATION_DIR"