Skip to content

Installer internals

install.sh is one bash script, written to run on bash 3.2 (macOS) and up, under set -euo pipefail:

  • -e stop at the first command that fails (unless the failure is handled)
  • -u treat unset variables as errors (catches typos)
  • -o pipefail a pipeline fails if any part fails, not just the last

Everything runs inside main, which is called on the very last line:

main
├── detect_system SYSTEM, NANO_SHARE, PKG_MGR
├── ask_questions SHELL_CHOICE, LOCALE (flags → prompts → defaults)
├── confirm_plan print plan, "Proceed? [Y/n]"
├── install_nano
├── install_tmux + TPM, Catppuccin (git clone)
├── ensure_nerd_font desktops only, chosen font → ~/.local/share/fonts
├── install_kitty + theme files, ensure_kitty_terminfo
├── install_nvim
├── install_starship
├── ensure_shell_tools less, bash-completion, notify-send
├── ensure_locale
├── write_locale_conf
├── ensure_zsh only if SHELL_CHOICE = zsh
├── install_shell_common .aliases, .functions
├── install_zsh .zshrc, .zshenv, .zprofile
├── install_bash .bash_profile, .bashrc, .inputrc
└── set_login_shell chsh

Why main? With curl … | bash, bash runs the script while it’s still downloading. Because only function definitions come before the last line, nothing runs until the whole file has arrived. A cut-off download can’t run half an install, and no command (like apt) can swallow the rest of the script by reading from stdin.

Variable Set by Values
SYSTEM detect_system linux, macos-intel, macos-arm
NANO_SHARE detect_system nano’s syntax folder for this platform
PKG_MGR detect_system apt, dnf, yum, pacman, zypper, apk, brew, or empty
MODE flags local or remote
SHELL_CHOICE flags / ask_questions zsh, bash, keep
LOCALE flags / ask_questions e.g. en_US.UTF-8
FONT_CHOICE flags / ask_questions Nerd Font archive name (Hasklig, FiraCode…) or none; always none when not a desktop
NERD_FONTS_VERSION top of file pinned Nerd Fonts release to download from
ASSUME_YES -y 0 / 1
DEFAULT_SHELL, DEFAULT_LOCALE top of file defaults for -y and no-terminal runs

On macOS only brew counts as a package manager; on Linux a Linuxbrew install is ignored so system packages go through the distro’s own manager.

Helper Job Rule
fetch_source <path> returns a local path for a repo file — the checkout in local mode, a downloaded temp copy in remote mode never use $REPO_DIR directly, so every config works in both modes
backup_if_exists <dest> moves an existing file to <dest>.bak.<timestamp> call before every write
install_home_files <files…> copy + back up a list of files to the same path under $HOME used by the shell installers
as_root <cmd…> runs a command directly when root, via sudo otherwise use for anything touching /etc
pkg_install <pkgs…> installs through $PKG_MGR; apt-get update once per run; pacman -Syu only on unsynced systems never call a package manager directly; returns non-zero instead of exiting
has_tty can we open /dev/tty?
can_prompt not -y and has_tty check before asking anything
ask_choice "<q>" <default> <opts…> numbered menu, result in REPLY_CHOICE the only way to prompt
normalize_locale ja_JP / ja_JP.utf8 → ja_JP.UTF-8, or fail
is_desktop macOS, or $DISPLAY / $WAYLAND_DISPLAY set gate desktop-only things

With curl … | bash, the script itself arrives on stdin. A plain read would take the next line of the script as the answer. /dev/tty is the keyboard directly.

Checking that /dev/tty exists isn’t enough: in Docker it exists but can’t be opened. So has_tty actually opens it:

Terminal window
has_tty() {
{ : </dev/tty; } 2>/dev/null # ':' does nothing; this only tests the open
}

Under set -e any failing command stops the script. So main runs every step through run_step: if a step fails (no network, a file that couldn’t be downloaded…), it prints a warning and goes on with the next step. At the end the script lists the failed steps and exits with 1, so scripts and CI notice:

Done, but these steps failed: install_tmux install_starship

Each step runs in a subshell, ( set -e; step ), so it still stops at its own first error. It can’t be written as step || warn: bash ignores set -e inside anything on the left of ||, so a broken step would carry on and look successful.

Inside a step, optional parts like package installs use pkg_install … || echo "warning: …", so a missing package doesn’t fail the whole step.

Paths that differ between systems are rewritten while copying, not kept as separate files. Example: nanorc contains the Linux path /usr/share/nano; install_nano swaps in $NANO_SHARE with sed. Add a new global in detect_system for any new platform-specific path.

Purpose apt dnf / yum pacman zypper apk
notify-send libnotify-bin libnotify libnotify libnotify-tools libnotify
kitty terminfo kitty-terminfo kitty-terminfo kitty-terminfo kitty-terminfo kitty-terminfo
locales locales + locale-gen glibc-langpack-<lang> locale-gen glibc-locale —
chsh (built in) util-linux-user (built in) (built in) shadow