Files
Panama/setup/scripts/interview
T
Gabriel Brown 88497826ec 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
2026-08-20 21:24:15 -04:00

215 lines
9.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Everything Panama needs to be told, asked before anything is installed.
#
# sunhat's failure mode was a question -- or a failure -- twenty minutes into a
# run, with a person needed at the keyboard to get past it. Walking away from an
# install meant coming back to a prompt that had been waiting an hour.
#
# So Panama asks first and then runs untouched. Everything interactive lives
# here, at the front, where the answers are cheap to change and nothing has been
# installed yet.
#
# Answers are NOT remembered between runs. There is no state file to go stale, and
# nothing personal is committed, which is what keeps this repository something
# 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. 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
# would do nothing to. Detection alone would be worse: it would remove the
# ability to decline a proprietary driver on a machine that has the card.
set -uo pipefail
# install passes the path. Refusing to guess one keeps the answers where the
# caller can delete them, rather than somewhere this script invented.
answers="${PANAMA_ANSWERS:-}"
[[ -n "$answers" ]] || { printf 'interview: PANAMA_ANSWERS is not set; run this through ./install\n' >&2; exit 1; }
: >"$answers"
# %q so a value containing a space, a quote or a dollar sign survives being
# sourced by install exactly as it was typed.
record() { printf '%s=%q\n' "$1" "$2" >>"$answers"; }
heading() { gum style --bold --foreground 4 "$1"; }
ask() { gum input --header "$1" --placeholder "${2:-}"; }
yes_no() { gum confirm --default=false "$1"; }
# ── Machine ──────────────────────────────────────────────────────────────────
heading "This machine"
current_hostname="$(hostname)"
printf 'Current hostname: %s\n' "$current_hostname"
new_hostname=""
if yes_no "Change the hostname?"; then
new_hostname="$(ask "Hostname" "$current_hostname")"
fi
record PANAMA_HOSTNAME "$new_hostname"
# ── Identity ─────────────────────────────────────────────────────────────────
#
# Left blank, each of these keeps whatever git already has. That matters on a
# re-run: the prompts start empty every time by design, and an empty answer must
# not wipe a name that was already correct.
heading "Git identity"
printf 'Leave any of these blank to keep the current setting.\n'
git_name="$(ask "Git user.name")"
git_email="$(ask "Git user.email")"
git_editor="$(ask "Git editor" "nvim")"
record PANAMA_GIT_NAME "$git_name"
record PANAMA_GIT_EMAIL "$git_email"
record PANAMA_GIT_EDITOR "$git_editor"
# ── Accounts and keys ────────────────────────────────────────────────────────
#
# These two are asked only when they would do something. Checking whether a
# credential already exists is not the same as remembering a previous answer --
# it is refusing to ask a question whose answer is already on the machine.
heading "Accounts"
gh_login=no
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
printf 'GitHub CLI is already signed in.\n'
elif yes_no "Sign in to GitHub after packages are installed?"; then
gh_login=yes
fi
record PANAMA_GH_LOGIN "$gh_login"
ssh_key=no
if compgen -G "$HOME/.ssh/id_*.pub" >/dev/null 2>&1; then
printf 'An SSH key already exists.\n'
elif yes_no "Generate an SSH key?"; then
ssh_key=yes
fi
record PANAMA_SSH_KEY "$ssh_key"
# ── Hardware ─────────────────────────────────────────────────────────────────
#
# Each question names what was detected, so declining is a decision about this
# machine rather than an answer to a hypothetical. A machine with no NVIDIA card
# is never asked about drivers, and one with nothing to remove is never asked
# about removing it.
heading "Hardware"
nvidia=no
mok_hash=""
nvidia_card="$(lspci 2>/dev/null | grep -iE 'vga compatible|3d controller' | grep -i nvidia | sed 's/.*: //' | head -1)"
if [[ -n "$nvidia_card" ]]; then
if yes_no "Found $nvidia_card — install the NVIDIA driver?"; then
nvidia=yes
# 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 enroll.\n'
printf 'The next boot will ask for this password on a blue screen.\n'
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 enrollment.\n'
break
fi
second="$(gum input --password --header "MOK password again")"
if [[ "$first" == "$second" ]]; then
mok_hash="$(mokutil --generate-hash="$first")"
break
fi
printf 'Those did not match.\n'
done
unset first second
fi
fi
fi
else
printf 'No NVIDIA card found.\n'
fi
record PANAMA_NVIDIA "$nvidia"
record PANAMA_MOK_HASH "$mok_hash"
# The stage that removes them owns the list, so there is one copy of it.
debloat=no
mapfile -t removable < <("$(dirname "${BASH_SOURCE[0]}")/install-hardware" --debloat-list)
installed=()
for package in "${removable[@]}"; do
rpm -q "$package" >/dev/null 2>&1 && installed+=("$package")
done
if (( ${#installed[@]} > 0 )); then
if yes_no "Remove Fedora's preinstalled extras (${installed[*]})?"; then
debloat=yes
fi
fi
record PANAMA_DEBLOAT "$debloat"
firmware=no
if command -v fwupdmgr >/dev/null 2>&1; then
if yes_no "Update firmware with fwupdmgr?"; then
firmware=yes
fi
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
# nobody is watching.
shown() { [[ -n "$1" ]] && printf '%s' "$1" || printf 'unchanged'; }
heading "Ready"
gum style --border rounded --padding "0 1" "$(
printf 'Hostname %s\n' "${new_hostname:-"$current_hostname (unchanged)"}"
printf 'Git name %s\n' "$(shown "$git_name")"
printf 'Git email %s\n' "$(shown "$git_email")"
printf 'Git editor %s\n' "$(shown "$git_editor")"
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 "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
printf 'interview: cancelled; nothing was installed.\n' >&2
exit 1
fi