fix(installer): flush stray tty input, split shell rc's from shell setup

The first checklist (Select Components) could silently auto-confirm its
defaults, swallowing the user's toggle: tui-install.sh runs right after the
long, mostly-unattended base install, and keystrokes buffered during that
wait were consumed by the script's very first read. Drain the tty before the
first prompt.

Also split the "shell" component into "shell" (tools/plugins/programs) and
a new "shellrc" (personal .bashrc/.zshrc/.vimrc), so the shell environment
can be installed without overwriting the user's rc files. shell-setup.sh
reads MARCHY_DEPLOY_SHELL_RC to gate the rc symlinks and now also deploys
.vimrc, which /etc/skel sync already expected but nothing ever created.
Removes the separate, easily-desynced "Shell Config for New Users" prompt
in favor of deriving /etc/skel behavior from the same shellrc choice, and
mirrors the removal in generate-answerfile.sh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
feat/astal-menu
Amir Alexander Abdelbaki 2026-07-03 14:22:03 +02:00
parent 01c58905b2
commit 2ffedd076e
3 changed files with 62 additions and 42 deletions

View File

@ -12,7 +12,8 @@
# ║ user — must NOT run as root. ║ # ║ user — must NOT run as root. ║
# ║ ║ # ║ ║
# ║ WHAT GETS DEPLOYED: ║ # ║ WHAT GETS DEPLOYED: ║
# ║ ~/.bashrc, ~/.zshrc — symlinks to Dotfiles repo ║ # ║ ~/.bashrc, ~/.zshrc, ~/.vimrc — symlinks to Dotfiles repo, unless ║
# ║ MARCHY_DEPLOY_SHELL_RC=0 (defaults to 1 outside the TUI installer) ║
# ║ ~/.config/starship.toml — symlink to repo ║ # ║ ~/.config/starship.toml — symlink to repo ║
# ║ ~/.config/nvim/ — symlink to repo/nvim/ ║ # ║ ~/.config/nvim/ — symlink to repo/nvim/ ║
# ║ ~/.config/micro/ — copy from repo/micro/ (plugin state needs cp)║ # ║ ~/.config/micro/ — copy from repo/micro/ (plugin state needs cp)║
@ -160,9 +161,18 @@ if [[ -e "$HOME/.config" && ! -w "$HOME/.config" ]]; then
sudo chown -R "$(id -un):$(id -gn)" "$HOME/.config" 2>/dev/null || true sudo chown -R "$(id -un):$(id -gn)" "$HOME/.config" 2>/dev/null || true
fi fi
# Shell init files — symlink so edits in the repo apply immediately # Shell init files — symlink so edits in the repo apply immediately.
ln -sf ~/Dotfiles/.bashrc ~/.bashrc # MARCHY_DEPLOY_SHELL_RC lets the caller install the shell tools/plugins below
ln -sf ~/Dotfiles/.zshrc ~/.zshrc # (zsh, oh-my-zsh, neovim, yazi, micro, starship, …) without overwriting the
# user's existing .bashrc/.zshrc/.vimrc with the_miro's personal versions.
# Defaults to deploying them when run standalone (outside the TUI installer).
if [[ "${MARCHY_DEPLOY_SHELL_RC:-1}" == "1" ]]; then
ln -sf ~/Dotfiles/.bashrc ~/.bashrc
ln -sf ~/Dotfiles/.zshrc ~/.zshrc
ln -sf ~/Dotfiles/.vimrc ~/.vimrc
else
skip "Personal shell rc's not selected — leaving .bashrc/.zshrc/.vimrc as-is."
fi
# Starship prompt config — symlink into ~/.config/ # Starship prompt config — symlink into ~/.config/
ln -sf ~/Dotfiles/starship.toml ~/.config/starship.toml ln -sf ~/Dotfiles/starship.toml ~/.config/starship.toml

View File

@ -201,7 +201,6 @@ dialog --backtitle "$BACKTITLE" \
AF_COMPONENTS="" AF_COMPONENTS=""
AF_DE="none" AF_DE="none"
AF_APPS="" AF_APPS=""
AF_SHELL_RC="dotfiles"
AF_COLOR_TEXT="" AF_COLOR_TEXT=""
AF_COLOR_BG="" AF_COLOR_BG=""
AF_COLOR_HIGHLIGHT="" AF_COLOR_HIGHLIGHT=""
@ -324,14 +323,6 @@ if [[ "$AF_RUN_TUI" == "true" ]]; then
3>&1 1>&2 2>&3) || AF_APPS="" 3>&1 1>&2 2>&3) || AF_APPS=""
# END GENERATED MODULES: module-checklist # END GENERATED MODULES: module-checklist
# ── Shell RC preference ───────────────────────────────────────────────────
AF_SHELL_RC=$(dialog --backtitle "$BACKTITLE" \
--title " Shell Config for New Users " \
--menu "\n Should new users on this machine inherit the dotfiles' rc files?\n (Controls what gets copied to /etc/skel)\n" 13 68 2 \
"dotfiles" "Use the_miro's .zshrc / .bashrc / .vimrc from dotfiles" \
"defaults" "Skip — use system defaults for new users" \
3>&1 1>&2 2>&3) || AF_SHELL_RC="defaults"
# ── Colorway ────────────────────────────────────────────────────────────── # ── Colorway ──────────────────────────────────────────────────────────────
# Read defaults from repo colors.conf # Read defaults from repo colors.conf
declare -A _cdef declare -A _cdef
@ -393,7 +384,6 @@ SUMMARY+=" Encrypt: $AF_ENCRYPT\n"
SUMMARY+=" FIDO2 root: $AF_FIDO2_ROOT / FIDO2 user: $AF_FIDO2_USER\n" SUMMARY+=" FIDO2 root: $AF_FIDO2_ROOT / FIDO2 user: $AF_FIDO2_USER\n"
SUMMARY+=" Run TUI: $AF_RUN_TUI\n" SUMMARY+=" Run TUI: $AF_RUN_TUI\n"
[[ -n "$AF_DE" && "$AF_DE" != "none" ]] && SUMMARY+=" DE: $AF_DE\n" [[ -n "$AF_DE" && "$AF_DE" != "none" ]] && SUMMARY+=" DE: $AF_DE\n"
SUMMARY+=" Shell rc: $AF_SHELL_RC\n"
[[ -n "$AF_COLOR_TEXT" ]] && SUMMARY+=" Colors: custom\n" [[ -n "$AF_COLOR_TEXT" ]] && SUMMARY+=" Colors: custom\n"
dialog --backtitle "$BACKTITLE" \ dialog --backtitle "$BACKTITLE" \
@ -435,8 +425,7 @@ mkdir -p "$(dirname "$OUTPUT")"
printf ' "run_tui": %s,\n' "$AF_RUN_TUI" printf ' "run_tui": %s,\n' "$AF_RUN_TUI"
printf ' "components": %s,\n' "$(_words_to_json_array "$AF_COMPONENTS")" printf ' "components": %s,\n' "$(_words_to_json_array "$AF_COMPONENTS")"
printf ' "desktop_environment": %s,\n' "$(json_str "$AF_DE")" printf ' "desktop_environment": %s,\n' "$(json_str "$AF_DE")"
printf ' "apps": %s,\n' "$(_words_to_json_array "$AF_APPS")" printf ' "apps": %s' "$(_words_to_json_array "$AF_APPS")"
printf ' "shell_rc": %s' "$(json_str "$AF_SHELL_RC")"
if [[ -n "$AF_COLOR_TEXT" ]]; then if [[ -n "$AF_COLOR_TEXT" ]]; then
printf ',\n "colors": {\n' printf ',\n "colors": {\n'

View File

@ -257,6 +257,18 @@ log_sep() {
printf "\n══════════════════════════════════\n %s\n %s\n" "$1" "$(date)" >> "$LOG" printf "\n══════════════════════════════════\n %s\n %s\n" "$1" "$(date)" >> "$LOG"
} }
flush_tty_input() {
# Discard any keystrokes queued on the terminal before the first prompt.
# This script normally runs right after the long, mostly-unattended base
# install (partitioning, pacstrap, …); impatient Enter presses or other
# stray input typed during that wait sit in the tty's input buffer and
# would otherwise be consumed by the very first `read` here — making the
# first checklist appear to auto-confirm its defaults before the user can
# toggle anything. A short non-blocking drain loop clears the queue.
local junk
while read -r -t 0.05 junk </dev/tty 2>/dev/null; do :; done
}
run_module() { run_module() {
local label="$1" script="$2" local label="$1" script="$2"
STEP=$(( STEP + 1 )) STEP=$(( STEP + 1 ))
@ -307,13 +319,14 @@ count_steps() {
# run_module() can display accurate [N/TOTAL] progress from the first step. # run_module() can display accurate [N/TOTAL] progress from the first step.
# Each glob test mirrors the corresponding run_module() call below; they # Each glob test mirrors the corresponding run_module() call below; they
# must stay in sync whenever a new module is added or removed. # must stay in sync whenever a new module is added or removed.
local c="$1" de="$2" a="${3:-}" local c=" $1 " de="$2" a="${3:-}"
TOTAL=0 TOTAL=0
# Base components: each keyword maps to exactly one module script. # Base components: each keyword maps to exactly one module script.
[[ "$c" == *"pkg"* ]] && TOTAL=$(( TOTAL + 1 )) # Padded with spaces so "shell" doesn't false-match the "shellrc" tag.
[[ "$c" == *"core"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$c" == *" pkg "* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$c" == *"svc"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$c" == *" core "* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$c" == *"shell"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$c" == *" svc "* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$c" == *" shell "* ]] && TOTAL=$(( TOTAL + 1 ))
# A non-"none" DE selection always installs exactly one DE module. # A non-"none" DE selection always installs exactly one DE module.
[[ "$de" != "none" ]] && TOTAL=$(( TOTAL + 1 )) [[ "$de" != "none" ]] && TOTAL=$(( TOTAL + 1 ))
# Optional app modules: one word-boundary check per app, one increment per match. # Optional app modules: one word-boundary check per app, one increment per match.
@ -410,7 +423,6 @@ count_steps() {
AF_COMPONENTS="" AF_COMPONENTS=""
AF_DE="none" AF_DE="none"
AF_APPS="" AF_APPS=""
AF_SHELL_RC="dotfiles"
AF_COLOR_TEXT="" AF_COLOR_TEXT=""
AF_COLOR_BG="" AF_COLOR_BG=""
AF_COLOR_HIGHLIGHT="" AF_COLOR_HIGHLIGHT=""
@ -427,7 +439,6 @@ load_answerfile() {
AF_COMPONENTS=$(jq -r '(.components // []) | join(" ")' "$ANSWERFILE") AF_COMPONENTS=$(jq -r '(.components // []) | join(" ")' "$ANSWERFILE")
AF_DE=$(jq -r '.desktop_environment // "none"' "$ANSWERFILE") AF_DE=$(jq -r '.desktop_environment // "none"' "$ANSWERFILE")
AF_APPS=$(jq -r '(.apps // []) | join(" ")' "$ANSWERFILE") AF_APPS=$(jq -r '(.apps // []) | join(" ")' "$ANSWERFILE")
AF_SHELL_RC=$(jq -r '.shell_rc // "dotfiles"' "$ANSWERFILE")
# Color values are optional; an empty string means "keep the repo default". # Color values are optional; an empty string means "keep the repo default".
AF_COLOR_TEXT=$(jq -r '.colors.COLOR_TEXT // ""' "$ANSWERFILE") AF_COLOR_TEXT=$(jq -r '.colors.COLOR_TEXT // ""' "$ANSWERFILE")
AF_COLOR_BG=$(jq -r '.colors.COLOR_BG // ""' "$ANSWERFILE") AF_COLOR_BG=$(jq -r '.colors.COLOR_BG // ""' "$ANSWERFILE")
@ -484,6 +495,7 @@ printf "Dotfiles install: %s\nDotfiles dir: %s\n" "$(date)" "$DOTFILES_DIR" >
# ── Welcome ─────────────────────────────────────────────────────────────────── # ── Welcome ───────────────────────────────────────────────────────────────────
if ! $ANSWERFILE_MODE; then if ! $ANSWERFILE_MODE; then
flush_tty_input
ui_msgbox " Welcome " "\ ui_msgbox " Welcome " "\
the_miro's Arch dotfiles installer\n\ the_miro's Arch dotfiles installer\n\
Cyberqueer · Wayland · Hyprland\n\ Cyberqueer · Wayland · Hyprland\n\
@ -503,13 +515,17 @@ if $ANSWERFILE_MODE; then
COMPONENTS="$AF_COMPONENTS" COMPONENTS="$AF_COMPONENTS"
else else
# ui_checklist takes tag/desc/state triplets and echoes the selected tags. # ui_checklist takes tag/desc/state triplets and echoes the selected tags.
# All four components are pre-selected ("on") because they form the expected base. # All five components are pre-selected ("on") because they form the expected base.
# "shellrc" is split out from "shell" so the shell tools/plugins/programs can be
# installed without symlinking the personal .bashrc/.zshrc/.vimrc over them —
# it only has an effect when "shell" is also selected.
# Entering 'q' returns exit code 1; the '|| { ...; exit 0; }' treats that as a clean abort. # Entering 'q' returns exit code 1; the '|| { ...; exit 0; }' treats that as a clean abort.
COMPONENTS=$(ui_checklist " Select Components " "Optional base components — all enabled by default:" \ COMPONENTS=$(ui_checklist " Select Components " "Optional base components — all enabled by default:" \
"pkg" "pkg Package managers yay · nvm · rust" on \ "pkg" "pkg Package managers yay · nvm · rust" on \
"core" "core Core packages 100+ base system packages" on \ "core" "core Core packages 100+ base system packages" on \
"svc" "svc Core services NetworkManager · cronie · fail2ban" on \ "svc" "svc Core services NetworkManager · cronie · fail2ban" on \
"shell" "shell Shell setup zsh · nvim · yazi · micro · starship" on) \ "shell" "shell Shell setup zsh · nvim · yazi · micro · starship" on \
"shellrc" "shellrc Personal shell rc's .bashrc/.zshrc/.vimrc (needs shell)" on) \
|| { clear; echo "Aborted."; exit 0; } || { clear; echo "Aborted."; exit 0; }
fi fi
@ -624,13 +640,13 @@ else
fi fi
# ── Shell RC preference ─────────────────────────────────────────────────────── # ── Shell RC preference ───────────────────────────────────────────────────────
if $ANSWERFILE_MODE; then # Derived from the "shellrc" component rather than asked separately: it only takes
SHELL_RC="$AF_SHELL_RC" # effect when "shell" also runs, and whether new users (via /etc/skel) inherit the
# rc files should follow the same choice as the current user's deployment.
if [[ " $COMPONENTS " == *" shell "* && " $COMPONENTS " == *" shellrc "* ]]; then
SHELL_RC="dotfiles"
else else
SHELL_RC=$(ui_menu " Shell Config for New Users " \ SHELL_RC="defaults"
"Should new users on this machine inherit the dotfiles' rc files?\n (Controls what gets copied to /etc/skel)" \
"dotfiles" "Use the_miro's .zshrc / .bashrc / .vimrc from dotfiles" \
"defaults" "Skip — use system defaults for new users") || SHELL_RC="defaults"
fi fi
# ── Confirmation (interactive mode only) ────────────────────────────────────── # ── Confirmation (interactive mode only) ──────────────────────────────────────
@ -638,13 +654,14 @@ if ! $ANSWERFILE_MODE; then
# Build a human-readable summary of everything that will be installed so the # Build a human-readable summary of everything that will be installed so the
# user can review the full list before any changes are made to the system. # user can review the full list before any changes are made to the system.
SUMMARY="" SUMMARY=""
[[ "$COMPONENTS" == *"pkg"* ]] && SUMMARY+=" ✦ Package managers (yay, nvm, rust)\n" [[ " $COMPONENTS " == *" pkg "* ]] && SUMMARY+=" ✦ Package managers (yay, nvm, rust)\n"
[[ "$COMPONENTS" == *"core"* ]] && SUMMARY+=" ✦ Core packages\n" [[ " $COMPONENTS " == *" core "* ]] && SUMMARY+=" ✦ Core packages\n"
[[ "$COMPONENTS" == *"svc"* ]] && SUMMARY+=" ✦ Core services\n" [[ " $COMPONENTS " == *" svc "* ]] && SUMMARY+=" ✦ Core services\n"
[[ "$COMPONENTS" == *"shell"* ]] && SUMMARY+=" ✦ Shell setup\n" [[ " $COMPONENTS " == *" shell "* ]] && SUMMARY+=" ✦ Shell setup (plugins & programs)\n"
[[ "$SHELL_RC" == "dotfiles" ]] && SUMMARY+=" ✦ Personal shell rc's (.bashrc/.zshrc/.vimrc)\n"
[[ "$DE" != "none" && "$DE" != "" ]] && SUMMARY+=" ✦ Desktop environment: $DE\n" [[ "$DE" != "none" && "$DE" != "" ]] && SUMMARY+=" ✦ Desktop environment: $DE\n"
[[ "$SHELL_RC" == "dotfiles" ]] && SUMMARY+=" ✦ Shell rc files → /etc/skel (dotfiles)\n" \ [[ "$SHELL_RC" == "dotfiles" ]] && SUMMARY+=" ✦ Shell rc's → /etc/skel for new users (dotfiles)\n" \
|| SUMMARY+=" ✦ Shell rc files → /etc/skel (system defaults)\n" || SUMMARY+=" ✦ Shell rc's → /etc/skel for new users (system defaults)\n"
if [[ -n "$SELECTED_APPS" ]]; then if [[ -n "$SELECTED_APPS" ]]; then
SUMMARY+="\n Applications:\n" SUMMARY+="\n Applications:\n"
@ -741,12 +758,16 @@ fi
count_steps "$COMPONENTS" "$DE" "$SELECTED_APPS" count_steps "$COMPONENTS" "$DE" "$SELECTED_APPS"
# ── Installation: base components ───────────────────────────────────────────── # ── Installation: base components ─────────────────────────────────────────────
# Each guard uses glob matching against the space-separated COMPONENTS string. # Each guard uses padded-space glob matching against the COMPONENTS string so
# "shell" doesn't false-match the "shellrc" tag.
# Order matters: package managers must be installed before packages that need yay/rust. # Order matters: package managers must be installed before packages that need yay/rust.
[[ "$COMPONENTS" == *"pkg"* ]] && run_module "Package Managers" "$MODULES/package-managers.sh" [[ " $COMPONENTS " == *" pkg "* ]] && run_module "Package Managers" "$MODULES/package-managers.sh"
[[ "$COMPONENTS" == *"core"* ]] && run_module "Core Packages" "$MODULES/core-packages.sh" [[ " $COMPONENTS " == *" core "* ]] && run_module "Core Packages" "$MODULES/core-packages.sh"
[[ "$COMPONENTS" == *"svc"* ]] && run_module "Core Services" "$MODULES/core.sh" [[ " $COMPONENTS " == *" svc "* ]] && run_module "Core Services" "$MODULES/core.sh"
[[ "$COMPONENTS" == *"shell"* ]] && run_module "Shell Setup" "$MODULES/shell-setup.sh" # MARCHY_DEPLOY_SHELL_RC tells shell-setup.sh whether to symlink the personal
# .bashrc/.zshrc/.vimrc, independently of installing the shell tools/plugins themselves.
[[ " $COMPONENTS " == *" shellrc "* ]] && export MARCHY_DEPLOY_SHELL_RC=1 || export MARCHY_DEPLOY_SHELL_RC=0
[[ " $COMPONENTS " == *" shell "* ]] && run_module "Shell Setup" "$MODULES/shell-setup.sh"
# Route the single selected DE value to its corresponding install script. # Route the single selected DE value to its corresponding install script.
# "none" is the skip sentinel — no case branch matches it intentionally. # "none" is the skip sentinel — no case branch matches it intentionally.