Compare commits

..
16 Commits
Author SHA1 Message Date
alvinlolloandClaude Opus 5.5 ad0f8291b7 zsh: install .zshenv, back it up first, dedupe config URL
- Add configs/.zshenv (skip_global_compinit=1) and download it in zsh.sh
- Back up existing ~/.zshenv alongside ~/.zshrc before overwriting
- Factor the raw config URL into CONFIG_BASE_URL
- Source zsh-autocomplete before oh-my-zsh instead of via plugins=()

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:21:06 +11:00
alvinlollo dafbfef5cd Add instruction files AGENTS.md and CLAUDE.md 2026-10-11 13:11:20 +11:00
alvinlolloandClaude Opus 5.5 4fa5bc7d60 installbeta: bootstrap shelly before gum, harden failure logging
- Abort with a clear log_fatal when the up-front sudo -v fails, instead
  of an unexplained errexit later on.
- Move the shelly bootstrap ahead of any gum use, and install gum from
  pacman in its own non-fatal transaction: a repo set without gum or a
  stale sync db must not take base-devel/git down with it. No -Sy (that
  would be a partial upgrade); the gum safety net retries via shelly,
  and gum is dropped from the shelly prerequisite list.
- run_step: spinner via gum spin when gum exists, plain otherwise. The
  step's output is captured and replayed on failure through stdout, so
  makepkg/git errors land in $LOG rather than only on the terminal.
- try_step: one helper for the run / "try it manually" hint /
  record_failure blocks (7 copies). The hint is built from the command
  that ran, so it cannot drift. makepkg now uses -D (pacman >= 7.0) so
  the build step is a single command.
- Sudo keep-alive loop stops its own sleep from a TERM trap, replacing
  the racy pkill -P in cleanup; its stderr stays out of the log.

Verified: bash -n + shellcheck clean; try_step/run_step and the loop's
shutdown exercised with stub commands. Not run end to end on Arch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:38:50 +11:00
alvinlollo a7bf7e5330 installbeta: gum log errors, package count in install summary
- Route failures through log_error/log_fatal (gum log --level error/fatal
  with kitchen timestamps and colour), falling back to plain echo before
  the gum bootstrap. log_fatal guards against gum's fatal level exiting 1
  under errexit. record_failure uses log_error; $REPORT keeps plain
  colour-free lines for the gum pager.
- Fatal level on the four aborting errors (gum install failures, gum
  required, fish-shell refusal), error level on the non-fatal headlines
  and invalid menu option; the "run it manually" command blocks stay as
  plain echo.
- Snapshot the package database (pacman, else rpm) before the first
  install step and report the net delta in the INSTALL SUMMARY. apt-only
  systems report nothing, by request.
2026-10-08 16:15:39 +11:00
alvinlollo ec5c598829 installbeta: gum TUI menu, sudo keep-alive loop, install summary
- Ask for sudo once up front and refresh the timestamp in the background
  (250s, sudo -n true only) so shelly/makepkg stay un-sudo'd and the user
  is never re-prompted mid-run; the loop is killed by a cleanup EXIT trap
  that also removes WORK_DIR.
- Replace the numbered text menu with gum choose, wrap sub-script runs in
  gum spin, and add sudo -v checkpoints before each privileged menu step.
- Add an INSTALL SUMMARY block (log/report paths, failure count).
- Move the fish-shell refusal after the gum bootstrap so it can always
  use gum confirm; drop the ERR trap and hand-rolled select_options now
  that errexit + gum cover those paths.
2026-10-08 16:12:45 +11:00
alvinlollo 8320546ad4 Fix fisher conflicts and stale end-4 paths found by the smoke test
First real run of tests/smoke.sh surfaced three issues:

- fish.sh: repo configs/ ships fisher.fish and nvm.fish functions, so the
  new (previously always-broken) 'fisher install' hit fisher's own
  conflict check and aborted. Now: check 'type -q fisher' / 'type -q nvm'
  inside fish and only bootstrap what is actually missing.
- fish.sh: end-4 renamed hypr/hyprland/colors.conf to colors.lua and
  moved hyprlock colors under hypr/hyprlock/ - the stale paths made the
  old script abort on cp. Updated paths, warn-and-skip stays as the
  safety net if upstream moves again.
- LazyVim.sh: same fisher conflict class in its fish branch - guard the
  nvm.fish install with a 'type -q nvm' check.
- tests/smoke.sh: the zsh backup assertion was wrong (fresh container has
  no .zshrc for run 1 to back up). Pre-seed a marker config instead and
  assert both rotation (2 backups) and that neither run destroys the
  original content.

Verified: tests/smoke.sh fish.sh zsh.sh -> PASS/PASS, bat.sh -> PASS;
pre-push hook green on all 9 scripts.
2026-10-08 14:26:56 +11:00
alvinlollo f8bb57f666 Add pre-push gate, container smoke test, README docs; drop vendored gum.md
- hooks/pre-push: bash -n + shellcheck -S warning -e SC1090 on every
  tracked *.sh; fails closed when shellcheck is missing, bypassable with
  --no-verify; enable with 'git config core.hooksPath hooks' (documented)
- tests/smoke.sh: manual docker/podman Arch-container run of bat.sh,
  zsh.sh and fish.sh with assertions (idempotent bat config, timestamped
  zsh backups, fisher actually installed in fish); LazyVim.sh opt-in.
  This catches the inverted-condition/quoting class shellcheck cannot.
  fish.sh gains a SKIP_END4_SETUP=1 test hook to skip end-4's installer
- README: scripts table (documents installbeta.sh vs install.sh and why
  SecureBoot.sh is not in the menu), apt full-upgrade note, development
  section; claims audited against current behavior (gum wording already
  correct, no whiptail/selection claims remain)
- remove untracked vendored gum.md (reference only, per decision)
2026-10-08 14:19:32 +11:00
alvinlollo bca2dd45ba zsh.sh nits: backup rotation, guarded rm, --needed, drop dead placeholder
- timestamped .zshrc backups (was: .zshrc.bak overwritten every run, so
  only the latest pre-run state survived)
- only remove ~/.oh-my-zsh when it actually contains oh-my-zsh.sh; warn
  and leave anything else alone (was: unguarded rm -rf)
- add --needed to the pacman prerequisite install
- drop the empty ~/.fzf/shell/key-bindings.zsh placeholder (nothing
  sourced it; fzf integration comes from the oh-my-zsh plugin)
- LICENSE spelling in banners (zsh.sh, installbeta.sh)
2026-10-08 14:16:37 +11:00
alvinlollo a012aeab9c installbeta.sh UX: docker group notice, drop fake command echoes and sleeps
- print a 'log out and back in' notice after usermod -aG docker (both
  fresh-install and already-installed branches; previously missing)
- remove the unexplained sleep 10 in the already-installed docker branch
- remove 'echo "+ sleep 10" && sleep 10' fake-command lines in failure
  branches (record_failure already reports the failure)
- affinity keeps its 10s read-the-instructions delay but announces it
  honestly instead of echoing a fake '+ 10 sleep' command
2026-10-08 14:15:00 +11:00
alvinlollo 23915c301a Strict mode from the start, contextual errors, non-zero exit in installbeta.sh
- set -Eeuo pipefail at the top (was only enabled after the menu, so
  package installs and shelly bootstrap ran unchecked); preamble audited
  for guarded vs unguarded commands ([ -t 1 ] && ... became an if, clear
  is now 'clear || true')
- error_handler now reports exit status, line number and the failing
  command, and the ERR trap is installed after strict mode so preamble
  failures abort instead of printing a message and continuing
- exit 1 when any step was recorded as failed (was: always exit 0)
- single STAMP shared by LOG and REPORT; both created via mktemp
  (predictable /tmp log names + tee -a was the same symlink class)
- local script/config fallbacks resolved via BASH_SOURCE (SCRIPT_DIR),
  not the caller's CWD; falls back to remote fetch when piped
- $? captured via rc=$? inside each failure branch (idempotent under
  reordering) for all record_failure call sites
- ${1:-} for the flag check, ${SHELL:-} in the fish detection,
  tty-guarded fish warning prompt
2026-10-08 14:13:57 +11:00
alvinlollo 0d605940b4 Harden installbeta.sh: temp workspace, curl -f, gum menu fallback
- mktemp -d workspace (trap-cleaned) for the shelly AUR clone, shelly toml
  downloads, docker installer and affinity script - no more predictable
  /tmp/shelly_install, /tmp/shelly-standard.toml, /tmp/shelly-aur.toml
- download get.docker.sh and AffinityLinuxInstaller.py with curl -fsSL to a
  file instead of piping to sh/python3 (the python pipe was missing -f, so an
  HTTP 404 body was executed as Python)
- plain numbered menu fallback when gum is unavailable (Fedora has neither
  shelly nor apt, so gum was never installed and 'gum choose' failed with a
  misleading 'User cancelled installation.')
- $? capture via rc=$? inside the failure branch (safe under reordering)
2026-10-08 14:10:43 +11:00
alvinlollo 696badb67a Fix LazyVim.sh shell branches, backups, and nvm detection
- swap the crossed branch tests (fish branch checked ~/.bashrc, bash branch
  checked ~/.config/fish/config.fish - each never matched its shell)
- install fisher/nvm.fish via 'fish -c' instead of calling fisher from bash
- timestamped backups (nvim.bak-YYYYmmdd-HHMMSS) instead of rm -rf of all
  *.bak dirs first - a re-run no longer destroys the original backup
- check git clone result and point at the preserved backup on failure
- load nvm by sourcing its init script (nvm is a function; 'command -v nvm'
  never succeeded, so the whole block was dead code)
- guard nvm init sourcing under set +u, fall back to system nodejs
- idempotent rc appends (grep guard), tty-guarded Ctrl+C grace period
- set -euo pipefail with guarded/unguarded audit
- typos: prerequisites, secconds
2026-10-08 14:08:20 +11:00
alvinlollo 99d94ab615 Fix fish.sh prerequisites, fisher install, and CWD-dependent paths
- prerequisite condition tested fish (was inverted: 'zsh missing and
  git/curl/fzf present', so fish was never installed on a working machine)
- run fisher bootstrap inside fish via 'fish -c' (was: 'curl | source' in
  bash, which always failed with 'filename argument required')
- download fisher.fish with curl -fsSL to a temp file first (fail on HTTP error)
- resolve configs/ via BASH_SOURCE; when piped (curl | bash) fall back to
  the end-4 clone instead of failing on relative paths from an arbitrary CWD
- clone dots-hyprland into mktemp -d, never into the caller's CWD; git -C
  style usage so a failed pull can't strand the script's working directory
- set -euo pipefail from the top with a guarded/unguarded audit
- warn visibly that end-4 still runs with --force --skip-backup (kept per decision)
- mkdir -p before config copies; tty-guarded fish warning; ${1:-}
2026-10-08 14:06:51 +11:00
alvinlollo 49e00110bf Fix bat.sh config target, theme name, MANPAGER quoting, shell detection
- set -euo pipefail with audit: every tolerated failure is guarded
- write --theme to bat's config file, not into the themes/ directory (was: append to a directory, failed silently)
- download and configure the same theme (Catppuccin Mocha; was: Macchiato downloaded, Mocha configured)
- preserve MANPAGER quoting via printf (was: collapsed to export MANPAGER=bat -plman, invalid when sourced)
- detect shell via $SHELL instead of $0 (was: always matched *) and reported 'Unsupported shell: bat.sh')
- fish branch appends fish syntax, no more exec of a mangled command string
- check pacman/apt bat install results (was: unchecked, continued on failure)
- idempotent appends (grep guard), tty-guarded fish warning
- remove broken rc-sourcing block (sourced .zshrc/config.fish from bash)
2026-10-08 14:04:49 +11:00
alvinlollo e215b85d90 Harden SecureBoot.sh: UEFI check, verify paths, confirm before enroll
- set -euo pipefail and fix pacman --noconfirm typo (was -noconfirm)
- abort on non-UEFI systems (/sys/firmware/efi check)
- verify sbctl installed and sign targets exist before touching keys
- fix x84_64-efi typo, skip missing targets instead of failing silently
- require sbctl verify to pass before offering enrollment
- prompt y/N before enroll-keys with BIOS/USB recovery guidance
- gate success message on final sbctl verify, non-zero exit on failure
2026-10-08 14:03:31 +11:00
alvinlollo a38a007ce3 Migrate menu from whiptail to gum with label-based dispatch 2026-10-08 14:02:43 +11:00
14 changed files with 1126 additions and 435 deletions
+16
View File
@@ -0,0 +1,16 @@
# AGENTS.md
## Environment & Constraints
* **Shell**: All scripts are written in `bash` and must be executed using `bash`. Do not attempt to run or source these scripts using `fish`, `zsh`, or other shells.
* **Platforms**: Primarily targeted at **Arch Linux**. Partial compatibility exists for Debian-based systems (specifically `zsh`, `bat`, `LazyVim` install scripts).
* **UI**: Expect interactive prompts using [`gum`](https://github.com/charmbracelet/gum) for selection menus.
## Repository Structure
* **`install.sh`**: The main entrypoint.
* **`zsh.sh`, `bat.sh`, `LazyVim.sh`, etc.**: Modular installation scripts for specific tools.
* **`configs/`**: Contains configuration files applied by the install scripts (e.g., `shelly-*.toml`).
## Developer Notes
* **Package Management**: Uses `shelly-cli` (Seafoam-Labs/Shelly-ALPM) for Arch Linux.
* **Debian Prerequisite**: On Debian-based systems, install scripts may trigger `apt full-upgrade -y`.
* **Testing**: If modifying scripts, verify behavior locally by sourcing or running in `bash`, ensuring `gum` is available for menu testing.
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+108 -87
View File
@@ -1,14 +1,18 @@
#!/usr/bin/bash
set -euo pipefail
# Detect fish shell and ask user to switch to bash (bash-only syntax)
if [ -n "${FISH_VERSION:-}" ] || case "$SHELL" in *fish*) ;; *) false ;; esac; then
if [ -n "${FISH_VERSION:-}" ] || case "${SHELL:-}" in *fish*) ;; *) false ;; esac; then
echo "Warning: fish shell detected."
echo "This script is written for bash and may have syntax issues under fish."
echo "Please switch to bash first and re-run it, e.g.: bash $0"
if [ -t 0 ] && [ -e /dev/tty ]; then
read -r -p "Press Enter to continue anyway, or Ctrl+C to cancel..." </dev/tty || true
fi
fi
# Install prerequisetes
# Install prerequisites
if command -v pacman >/dev/null; then
echo "pacman detected. Installing prerequisites"
# sclip is not packaged for arch, tree-sitter-cli ships the tree-sitter binary
@@ -48,61 +52,34 @@ elif command -v apt >/dev/null; then
else
echo "Cannot proceed: pacman or apt is required"
echo "This script supports arch based and debian based systems"
echo "+ sleep 10" && sleep 10
exit 1
fi
echo ""
echo 'This script will move your current nvim config to ~/.config/nvim.bak'
echo "Press CTRL+C within 10 secconds if you don't want these actions to be made"
sleep 10
echo 'This script will back up your current nvim config with a timestamped name, e.g. ~/.config/nvim.bak-20261008-130000'
if [ -t 0 ]; then
echo "Press CTRL+C within 10 seconds if you don't want these actions to be made"
sleep 10
fi
echo ""
# Remove old backups
# Remove existing .bak directories first to ensure a clean slate
echo " Checking for and removing old Neovim backup directories..."
# Back up current nvim directories with a timestamp so a re-run never
# destroys the user's original backup (old behaviour deleted *.bak first)
STAMP="$(date +%Y%m%d-%H%M%S)"
# Use $HOME for reliable path expansion
if [ -e "$HOME/.config/nvim.bak" ]; then
echo "Path $HOME/.config/nvim.bak exists. Removing..."
rm -rf "$HOME/.config/nvim.bak"
fi
backup_if_exists() {
local src="$1"
if [ -e "$src" ]; then
echo " Backing up $src -> ${src}.bak-$STAMP"
mv "$src" "${src}.bak-$STAMP"
fi
}
if [ -e "$HOME/.local/share/nvim.bak" ]; then
echo "Path $HOME/.local/share/nvim.bak exists. Removing..."
rm -rf "$HOME/.local/share/nvim.bak"
fi
if [ -e "$HOME/.local/state/nvim.bak" ]; then
echo "Path $HOME/.local/state/nvim.bak exists. Removing..."
rm -rf "$HOME/.local/state/nvim.bak"
fi
if [ -e "$HOME/.cache/nvim.bak" ]; then
echo "Path $HOME/.cache/nvim.bak exists. Removing..."
rm -rf "$HOME/.cache/nvim.bak"
fi
# Remove the current nvim config and create a new backup
echo ""
echo " Backing up current Neovim configuration..."
if [ -d "$HOME/.config/nvim" ]; then # Check if nvim config exists before moving
mv "$HOME/.config/nvim" "$HOME/.config/nvim.bak"
else
echo "No existing ~/.config/nvim found to back up."
fi
echo ""
echo " Backing up Neovim share, state, and cache directories..."
if [ -d "$HOME/.local/share/nvim" ]; then
mv "$HOME/.local/share/nvim" "$HOME/.local/share/nvim.bak"
fi
if [ -d "$HOME/.local/state/nvim" ]; then
mv "$HOME/.local/state/nvim" "$HOME/.local/state/nvim.bak"
fi
if [ -d "$HOME/.cache/nvim" ]; then
mv "$HOME/.cache/nvim" "$HOME/.cache/nvim.bak"
fi
backup_if_exists "$HOME/.config/nvim"
backup_if_exists "$HOME/.local/share/nvim"
backup_if_exists "$HOME/.local/state/nvim"
backup_if_exists "$HOME/.cache/nvim"
# Remove any leftover
if [ -e "$HOME/.config/.nvim" ]; then
@@ -113,51 +90,95 @@ fi
# Install LazyVim
echo ""
git clone https://github.com/alvinlollo/LazyVim ~/.config/nvim
# Check if nvm is in shell path
if command -v nvm &>/dev/null; then
# Get current shell name
CURRENT_SHELL=$(basename -- "$SHELL")
# 2. Source the correct configuration file safely
if [ "$CURRENT_SHELL" = "fish" ] && [ -f ~/.bashrc ]; then
# Check if fish is installed first, then check for fisher
if command -v fish &>/dev/null && fish -c "functions -q fisher" &>/dev/null; then
fisher install jorgebucaran/nvm.fish
source ~/.config/fish/config.fish
else
curl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish | source
fisher install jorgebucaran/fisher
fisher install jorgebucaran/nvm.fish
source ~/.config/fish/config.fish
fi
elif [ "$CURRENT_SHELL" = "zsh" ] && [ -f ~/.zshrc ]; then
echo -e "export NVM_DIR=\"$HOME/.nvm\"\n[ -s \"/usr/share/nvm/nvm.sh\" ] && \. \"/usr/share/nvm/nvm.sh\"\n[ -s \"/usr/share/nvm/init-nvm.sh\" ] && \. \"/usr/share/nvm/init-nvm.sh\"" >>~/.zshrc
source ~/.zshrc
elif [ "$CURRENT_SHELL" = "bash" ] && [ -f ~/.config/fish/config.fish ]; then
echo "source /usr/share/nvm/init-nvm.sh" >>~/.bashrc
source ~/.bashrc
else
echo "Unsupported or unreadable shell configuration: $CURRENT_SHELL"
echo "Failed to add NVM to shell path. Please add it manually to your shell configuration file."
fi
if ! git clone https://github.com/alvinlollo/LazyVim "$HOME/.config/nvim"; then
echo "error: failed to clone https://github.com/alvinlollo/LazyVim into ~/.config/nvim" >&2
echo "Your previous config is preserved in ~/.config/nvim.bak-$STAMP (if one existed)." >&2
exit 1
fi
# Use NVM to install the latest LTS version of Node.js
if command -v nvm &>/dev/null; then
# Load nvm into this shell if it exists. nvm is a shell function, so
# 'command -v nvm' only works after its init script has been sourced.
NVM_LOADED=false
for nvm_init in "$HOME/.nvm/nvm.sh" /usr/share/nvm/nvm.sh /usr/share/nvm/init-nvm.sh; do
if [ -f "$nvm_init" ]; then
set +u # nvm's init script is not always nounset-safe
# shellcheck disable=SC1090
if ! . "$nvm_init"; then
set -u
continue # try the next candidate
fi
set -u
NVM_LOADED=true
break
fi
done
CURRENT_SHELL=$(basename -- "${SHELL:-/bin/bash}")
case "$CURRENT_SHELL" in
fish)
# fish cannot source nvm.sh - it uses the nvm.fish plugin instead
if [ -f "$HOME/.config/fish/config.fish" ]; then
if ! fish -c 'type -q fisher' 2>/dev/null; then
fisher_src="$(mktemp)"
if ! curl -fsSL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish -o "$fisher_src"; then
echo "error: failed to download fisher.fish" >&2
rm -f "$fisher_src"
exit 1
fi
fish -c "source '$fisher_src' && fisher install jorgebucaran/fisher"
rm -f "$fisher_src"
fi
# fisher refuses to install over existing files - skip when already present
if fish -c 'type -q nvm' 2>/dev/null; then
echo "nvm.fish already available in fish, skipping install."
else
fish -c 'fisher install jorgebucaran/nvm.fish'
fi
else
echo "No ~/.config/fish/config.fish found, skipping fish plugin setup"
fi
;;
zsh)
if [ -f "$HOME/.zshrc" ]; then
if ! grep -q 'nvm.sh' "$HOME/.zshrc"; then
printf '%s\n' \
'export NVM_DIR="$HOME/.nvm"' \
'[ -s "/usr/share/nvm/nvm.sh" ] && . "/usr/share/nvm/nvm.sh"' \
'[ -s "/usr/share/nvm/init-nvm.sh" ] && . "/usr/share/nvm/init-nvm.sh"' >>"$HOME/.zshrc"
fi
else
echo "No ~/.zshrc found, skipping nvm shell wiring"
fi
;;
bash)
if [ -f "$HOME/.bashrc" ]; then
if ! grep -q 'nvm.sh\|init-nvm.sh' "$HOME/.bashrc"; then
printf '%s\n' \
'export NVM_DIR="$HOME/.nvm"' \
'[ -s "/usr/share/nvm/nvm.sh" ] && . "/usr/share/nvm/nvm.sh"' \
'[ -s "/usr/share/nvm/init-nvm.sh" ] && . "/usr/share/nvm/init-nvm.sh"' >>"$HOME/.bashrc"
fi
else
echo "No ~/.bashrc found, skipping nvm shell wiring"
fi
;;
*)
echo "Unsupported or unreadable shell configuration: $CURRENT_SHELL"
echo "Failed to add NVM to shell path. Please add it manually to your shell configuration file."
;;
esac
# Use nvm (when loaded) to install the latest LTS version of Node.js
if [ "$NVM_LOADED" = true ]; then
nvm install lts
nvm use lts
elif command -v node >/dev/null; then
echo "nvm not found, using system Node.js $(node --version)"
echo "nvm not available, using system Node.js $(node --version)"
else
echo "No Node.js installation found."
echo "Source your nvm install (e.g. . /usr/share/nvm/nvm.sh) or install nodejs, then re-run this script."
echo "Install nvm (Arch: pacman -S nvm) or nodejs, then re-run this script."
exit 1
fi
echo "LazyVim setup complete."
+31 -2
View File
@@ -1,7 +1,7 @@
# Single-install-script
This script automatically installs my favourite Linux applications I use every day such as:
[Docker](https://www.docker.com/), [zsh, oh-my-zsh](https://github.com/ohmyzsh/ohmyzsh) and [neovim](https://neovim.io/). This uses the [shelly-cli](https://github.com/Seafoam-Labs/Shelly-ALPM) package manager. You can choose what sections to install with the whiptail menu.
[Docker](https://www.docker.com/), [zsh, oh-my-zsh](https://github.com/ohmyzsh/ohmyzsh) and [neovim](https://neovim.io/). This uses the [shelly-cli](https://github.com/Seafoam-Labs/Shelly-ALPM) package manager. You can choose what sections to install with the [gum](https://github.com/charmbracelet/gum) menu.
This script will automatically install [shelly-cli](https://github.com/Seafoam-Labs/Shelly-ALPM) if it is not already present.
@@ -15,7 +15,7 @@ Please consider staring this project. It helps me see how important this is for
## Usage
> **Warning:** Run this script with **bash**, not fish (or another shell). It uses bash-only syntax and will fail if run under fish. Keep `bash` in the commands below — do not replace it with `fish` or `source` the script. If fish is your default shell, a warning (dialog if whiptail is installed, otherwise a text prompt) will remind you before anything is installed.
> **Warning:** Run this script with **bash**, not fish (or another shell). It uses bash-only syntax and will fail if run under fish. Keep `bash` in the commands below — do not replace it with `fish` or `source` the script. If fish is your default shell, a warning (a `gum confirm` dialog if gum is installed, otherwise a text prompt) will remind you before anything is installed.
To use this script paste the following in the Terminal:
@@ -29,6 +29,35 @@ For installing zsh and themes (also works on Debian based systems):
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/zsh.sh | bash
```
> **Note:** on Debian-based systems the installer runs `apt full-upgrade -y` as part of its prerequisites. On Arch it installs prerequisites through shelly without upgrading the whole system.
## Scripts
| Script | Purpose |
| -------------- | ----------------------------------------------------------------------- |
| `install.sh` | Main menu (gum) that dispatches to the scripts below. |
| `installbeta.sh` | Same menu, but logs every step and records failures instead of aborting, then exits non-zero if anything failed. This is the next `install.sh` — once tested it replaces it. |
| `zsh.sh` | zsh + oh-my-zsh + plugins and my config (also works on Debian). |
| `fish.sh` | fish + Fisher plugins + end-4 dotfiles config. |
| `LazyVim.sh` | My LazyVim neovim config. |
| `bat.sh` | bat + Catppuccin Mocha theme + MANPAGER setup. |
| `SecureBoot.sh`| **Not in the menu** (intentional): manual UEFI-only Secure Boot setup with `sbctl`. It verifies every sign target and asks before enrolling keys — read it before running. |
## Development
Enable the pre-push gate once per clone (runs `bash -n` and `shellcheck` on every tracked `*.sh`):
```bash
git config core.hooksPath hooks
```
There is also a manual end-to-end smoke test that runs the installers in a throwaway Arch container (needs docker or podman, network access; **not** part of the hook):
```bash
tests/smoke.sh # bat.sh, zsh.sh, fish.sh
tests/smoke.sh LazyVim.sh # opt in to the heavy LazyVim run
```
## Zsh configuration
My Zsh configuration includes the following plugins run through [oh-my-zsh](https://ohmyz.sh/): git, [zsh-history-substring-search](https://github.com/zsh-users/zsh-history-substring-search), [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions), [zsh-autocomplete](https://github.com/marlonrichert/zsh-autocomplete), [fzf-zsh-plugin](https://github.com/unixorn/fzf-zsh-plugin) and [zsh-syntax-highlighting](https://github.com/zsh-users/zsh-syntax-highlighting.git)
+98 -12
View File
@@ -1,25 +1,111 @@
#!/usr/bin/bash
set -euo pipefail
# Detect fish shell and ask user to switch to bash (bash-only syntax)
if [ -n "${FISH_VERSION:-}" ] || case "$SHELL" in *fish*) ;; *) false ;; esac; then
if [ -n "${FISH_VERSION:-}" ] || case "${SHELL:-}" in *fish*) ;; *) false ;; esac; then
echo "Warning: fish shell detected."
echo "This script is written for bash and may have syntax issues under fish."
echo "Please switch to bash first and re-run it, e.g.: bash $0"
if [ -t 0 ] && [ -e /dev/tty ]; then
read -r -p "Press Enter to continue anyway, or Ctrl+C to cancel..." </dev/tty || true
fi
fi
# Install Prerequisites
sudo pacman -S --needed -noconfirm efibootmgr sbsigntools mokutil sbctl grub
# Secure Boot setup only applies to UEFI systems (meaningless on BIOS/CSM)
if [ ! -d /sys/firmware/efi ]; then
echo "error: this system is not booted in UEFI mode (/sys/firmware/efi is missing)." >&2
echo "Secure Boot setup does not apply to BIOS/CSM machines. Aborting." >&2
exit 1
fi
if ! command -v pacman >/dev/null; then
echo "error: pacman not found. This script only supports Arch-based systems." >&2
exit 1
fi
# Install prerequisites
sudo pacman -S --needed --noconfirm efibootmgr sbsigntools mokutil sbctl grub
# Fail early if the toolchain is still missing (do not touch keys otherwise)
if ! command -v sbctl >/dev/null; then
echo "error: sbctl is not available after installation. Install it manually and re-run." >&2
exit 1
fi
# Candidate sign targets. GRUB's path and the kernel image name vary by
# machine, so every path is checked before signing instead of trusted blindly.
SIGN_TARGETS=(
/boot/EFI/BOOT/BOOTX64.EFI
/boot/grub/x86_64-efi/core.efi
/boot/grub/x86_64-efi/grub.efi
/boot/vmlinuz-linux
)
missing=0
for target in "${SIGN_TARGETS[@]}"; do
if [ ! -e "$target" ]; then
echo "warning: $target does not exist on this machine, skipping" >&2
missing=$((missing + 1))
fi
done
if [ "$missing" -eq "${#SIGN_TARGETS[@]}" ]; then
echo "error: none of the expected boot files exist; refusing to continue." >&2
echo "Adjust SIGN_TARGETS for this machine and re-run." >&2
exit 1
fi
sudo sbctl create-keys
sudo sbctl enroll-keys --microsoft
sudo sbctl verify
# use sudo sbctl sign -s for every unsigned item
sudo sbctl sign -s /boot/EFI/BOOT/BOOTX64.EFI
sudo sbctl sign -s /boot/grub/x84_64-efi/core.efi
sudo sbctl sign -s /boot/grub/x86_64-efi/grub.efi
sudo sbctl sign -s /boot/vmlinuz-linux
for target in "${SIGN_TARGETS[@]}"; do
if [ -e "$target" ]; then
sudo sbctl sign -s "$target"
fi
done
# Every file must verify BEFORE enrollment - enrollment is hard to undo
echo "Verifying signatures before enrollment..."
if ! sudo sbctl verify; then
echo "error: sbctl verify reports unsigned or badly signed files." >&2
echo "Fix these first (sudo sbctl sign -s <file>). Not enrolling." >&2
exit 1
fi
echo ""
echo "All boot files are signed and verified."
echo ""
echo "Before enrolling keys, be ready for what can go wrong:"
echo " - After enrollment, any file not signed with these keys will not boot."
echo " - To recover: enter BIOS and disable Secure Boot (or boot a USB stick),"
echo " then run 'sudo sbctl unenroll' or re-sign the missing files."
echo " - Keep a bootable USB / BIOS access available for the first reboot."
echo ""
# Only enroll when a human explicitly says so, and only when we have a terminal
enroll=""
if [ -t 0 ] && [ -e /dev/tty ]; then
read -r -p "Enroll Secure Boot keys now? [y/N] " enroll </dev/tty || enroll=""
fi
case "$enroll" in
y | Y | yes | YES)
sudo sbctl enroll-keys --microsoft
;;
*)
echo "Enrollment skipped. Nothing has been enrolled."
echo "When you are ready, run: sudo sbctl enroll-keys --microsoft"
exit 0
;;
esac
sudo sbctl status
sudo sbctl verify
echo "Use sudo sbctl sign -s for every unsigned item"
if sudo sbctl verify; then
echo ""
echo "Success: every file is signed and verified."
echo "Reboot and confirm the machine boots with Secure Boot enabled."
else
echo "error: verification failed after signing. Do not reboot yet." >&2
exit 1
fi
+50 -35
View File
@@ -1,15 +1,19 @@
#!/usr/bin/bash
set -euo pipefail
# Detect fish shell and ask user to switch to bash (bash-only syntax)
if [ -n "${FISH_VERSION:-}" ] || case "$SHELL" in *fish*) ;; *) false ;; esac; then
if [ -n "${FISH_VERSION:-}" ] || case "${SHELL:-}" in *fish*) ;; *) false ;; esac; then
echo "Warning: fish shell detected."
echo "This script is written for bash and may have syntax issues under fish."
echo "Please switch to bash first and re-run it, e.g.: bash $0"
if [ -t 0 ] && [ -e /dev/tty ]; then
read -r -p "Press Enter to continue anyway, or Ctrl+C to cancel..." </dev/tty || true
fi
fi
skip_watermark=false
if [ "$1" = "--skip-watermark" ]; then
if [ "${1:-}" = "--skip-watermark" ]; then
skip_watermark=true
fi
@@ -23,8 +27,8 @@ if [ "$skip_watermark" = false ]; then
|___/
--------------- bat Install Script ---------------
BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE UNDER THE GPL-2.0 LICENCE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. See the LICENCE for more detail
BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE UNDER THE GPL-2.0 LICENSE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. See the LICENSE for more detail
'
fi
@@ -50,20 +54,24 @@ if ! command -v git >/dev/null || ! command -v curl >/dev/null || ! command -v w
fi
# Install bat
fresh_bat_install=false
if ! command -v bat >/dev/null && ! command -v batcat >/dev/null; then
if command -v pacman >/dev/null; then
echo "pacman detected. Installing bat"
sudo pacman -S --needed --noconfirm bat bat-extras
if ! sudo pacman -S --needed --noconfirm bat bat-extras; then
echo "Failed to install bat. Install it manually, then re-run this script."
exit 1
fi
elif command -v apt >/dev/null; then
echo "apt detected. Installing bat"
sudo apt install bat -y
if ! sudo apt install bat -y; then
echo "Failed to install bat. Install it manually, then re-run this script."
exit 1
fi
else
echo "No supported package manager found (pacman/apt)."
echo "Please install bat manually, then re-run this script."
exit 1
fi
fresh_bat_install=true
fi
# Debian/Ubuntu ship the binary as batcat, provide bat for the commands below
@@ -76,38 +84,45 @@ if ! command -v bat >/dev/null; then
exit 1
fi
if [ "$fresh_bat_install" = true ]; then
# 1. Detect the actual active shell name (not just the login default)
CURRENT_SHELL=$(basename -- "$SHELL")
# 2. Source the correct configuration file safely
if [ "$CURRENT_SHELL" = "bash" ] && [ -f ~/.bashrc ]; then
source ~/.bashrc
elif [ "$CURRENT_SHELL" = "zsh" ] && [ -f ~/.zshrc ]; then
source ~/.zshrc
elif [ "$CURRENT_SHELL" = "fish" ] && [ -f ~/.config/fish/config.fish ]; then
source ~/.config/fish/config.fish
else
echo "Unsupported or unreadable shell configuration: $CURRENT_SHELL"
# Install the Catppuccin Mocha theme (downloaded and configured names must match)
BAT_CONFIG_DIR="$(bat --config-dir)"
mkdir -p "$BAT_CONFIG_DIR/themes"
if [ ! -f "$BAT_CONFIG_DIR/themes/Catppuccin Mocha.tmTheme" ]; then
if ! wget -q -O "$BAT_CONFIG_DIR/themes/Catppuccin Mocha.tmTheme" \
"https://github.com/catppuccin/bat/raw/main/themes/Catppuccin%20Mocha.tmTheme"; then
echo "Failed to download the Catppuccin Mocha theme."
exit 1
fi
fi
# Install Catppuccin themes
source ~/.bashrc
mkdir -p "$(bat --config-dir)/themes"
wget -P "$(bat --config-dir)/themes" https://github.com/catppuccin/bat/raw/main/themes/Catppuccin%20Macchiato.tmTheme
bat cache --build
echo "--theme="Catppuccin Mocha"" >>"$(bat --config-dir)/themes"
# Set the default theme in bat's config file (the themes/ directory only holds themes)
if ! grep -qxF -- '--theme="Catppuccin Mocha"' "$BAT_CONFIG_DIR/config" 2>/dev/null; then
printf '%s\n' '--theme="Catppuccin Mocha"' >>"$BAT_CONFIG_DIR/config"
fi
# Detect shell name via POSIX standard
CURRENT_SHELL=$(basename "$0" 2>/dev/null || echo "$0")
CURRENT_SHELL="${CURRENT_SHELL#-}"
# Configure the MANPAGER for the user's actual login shell ($SHELL, not $0)
CURRENT_SHELL=$(basename -- "${SHELL:-/bin/bash}")
case "$CURRENT_SHELL" in
"bash") [ -f ~/.bashrc ] && echo "export MANPAGER="bat -plman"" >>~/.bashrc ;;
"zsh") [ -f ~/.zshrc ] && echo "export MANPAGER="bat -plman"" >>~/.zshrc ;;
"fish") exec fish -c "echo "export MANPAGER="bat -plman"" >> ~/.config/fish/config.fish" ;;
*) echo "Unsupported shell: $CURRENT_SHELL" ;;
bash)
if [ -f "$HOME/.bashrc" ] && ! grep -qxF 'export MANPAGER="bat -plman"' "$HOME/.bashrc"; then
printf '%s\n' 'export MANPAGER="bat -plman"' >>"$HOME/.bashrc"
fi
;;
zsh)
if [ -f "$HOME/.zshrc" ] && ! grep -qxF 'export MANPAGER="bat -plman"' "$HOME/.zshrc"; then
printf '%s\n' 'export MANPAGER="bat -plman"' >>"$HOME/.zshrc"
fi
;;
fish)
if [ -f "$HOME/.config/fish/config.fish" ] && ! grep -qxF 'set -gx MANPAGER "bat -plman"' "$HOME/.config/fish/config.fish"; then
printf '%s\n' 'set -gx MANPAGER "bat -plman"' >>"$HOME/.config/fish/config.fish"
fi
;;
*)
echo "Unsupported shell: $CURRENT_SHELL (skipping MANPAGER setup)"
;;
esac
echo "bat configured with the Catppuccin Mocha theme."
+1
View File
@@ -0,0 +1 @@
skip_global_compinit=1
+2 -1
View File
@@ -18,6 +18,8 @@ fi
# Path to your oh-my-zsh installation.
export ZSH=~/.oh-my-zsh
source $ZSH/custom/plugins/zsh-autocomplete/zsh-autocomplete.plugin.zsh
# Set the name of the theme to load. If you set this to "random"
# It'll load a random theme each time that oh-my-zsh is loaded.
# See https://github.com/robbyrussell/oh-my-zsh/wiki/Themes
@@ -79,7 +81,6 @@ plugins=(
git
zsh-history-substring-search
zsh-autosuggestions
zsh-autocomplete
# zsh-eza
fzf-zsh-plugin
zsh-syntax-highlighting
+134 -64
View File
@@ -1,15 +1,19 @@
#!/usr/bin/bash
set -euo pipefail
# Detect fish shell and ask user to switch to bash (bash-only syntax)
if [ -n "${FISH_VERSION:-}" ] || case "$SHELL" in *fish*) ;; *) false ;; esac; then
if [ -n "${FISH_VERSION:-}" ] || case "${SHELL:-}" in *fish*) ;; *) false ;; esac; then
echo "Warning: fish shell detected."
echo "This script is written for bash and may have syntax issues under fish."
echo "Please switch to bash first and re-run it, e.g.: bash $0"
if [ -t 0 ] && [ -e /dev/tty ]; then
read -r -p "Press Enter to continue anyway, or Ctrl+C to cancel..." </dev/tty || true
fi
fi
skip_watermark=false
if [ "$1" = "--skip-watermark" ]; then
if [ "${1:-}" = "--skip-watermark" ]; then
skip_watermark=true
fi
@@ -23,94 +27,160 @@ if [ "$skip_watermark" = false ]; then
|___/
--------------- FISH Install Script ---------------
BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE UNDER THE GPL-2.0 LICENCE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. See the LICENCE for more detail
BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE UNDER THE GPL-2.0 LICENSE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. See the LICENSE for more detail
'
fi
# Show disclaimer
echo "This script will add fish functions and install fish"
# Enable exit on error
set -eu
# Resolve where this script (and its configs/) live. When the script arrives
# over a pipe (curl | bash) there is no local checkout, so repo-config syncing
# is skipped and only the user's home directory is populated.
if [ -n "${BASH_SOURCE[0]:-}" ] && [ -f "${BASH_SOURCE[0]}" ]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
else
SCRIPT_DIR=""
fi
# Install prerequisites if installed skips
# Never clone or write into the caller's current directory
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT
if ! command -v zsh >/dev/null && command -v git >/dev/null && command -v curl >/dev/null && command -v fzf >/dev/null; then
# Install prerequisites if any of them are missing
if ! command -v fish >/dev/null || ! command -v git >/dev/null ||
! command -v curl >/dev/null || ! command -v fzf >/dev/null; then
if command -v pacman >/dev/null; then
echo "pacman detected. Installing prerequisites"
sudo pacman -S fish git curl fzf --noconfirm
if ! sudo pacman -S --needed --noconfirm fish git curl fzf; then
echo "Failed to install prerequisites (fish, git, curl, fzf). Install them manually, then re-run this script."
exit 1
fi
if command -v apt >/dev/null; then
elif command -v apt >/dev/null; then
echo "apt detected. Installing prerequisites"
sudo apt install git curl fish fzf -y
if ! sudo apt update || ! sudo apt install git curl fish fzf -y; then
echo "Failed to install prerequisites (fish, git, curl, fzf). Install them manually, then re-run this script."
exit 1
fi
else
echo "No supported package manager found (pacman/apt)."
echo "Please install fish, git, curl and fzf manually, then re-run this script."
exit 1
fi
fi
# Clone or pull end-4 dotfiles repository
if [ -d dots-hyprland ]; then
cd dots-hyprland && git pull && cd ..
if ! command -v fish >/dev/null; then
echo "error: fish could not be installed (no supported package manager, or install failed)." >&2
echo "Install fish manually, then re-run this script." >&2
exit 1
fi
# Clone the end-4 dotfiles repository into the temp workspace
if ! git clone --depth=1 https://github.com/end-4/dots-hyprland.git "$WORK_DIR/dots-hyprland"; then
echo "error: failed to clone https://github.com/end-4/dots-hyprland.git" >&2
exit 1
fi
# Sync fresh end-4 files into this repo's configs/ (only when running from a
# local checkout, and only for files that do not exist yet)
if [ -n "$SCRIPT_DIR" ] && [ -d "$SCRIPT_DIR/configs" ]; then
for rel in \
fish/config.fish \
fish/auto-Hypr.fish \
fish/fish_variables \
hypr/hyprland/colors.lua \
hypr/hyprlock/colors.conf; do
if [ ! -f "$SCRIPT_DIR/configs/$rel" ] && [ -f "$WORK_DIR/dots-hyprland/dots/.config/$rel" ]; then
mkdir -p "$(dirname "$SCRIPT_DIR/configs/$rel")"
cp "$WORK_DIR/dots-hyprland/dots/.config/$rel" "$SCRIPT_DIR/configs/$rel"
fi
done
fi
# Run end-4 setup script with flags to skip unnecessary components.
# SKIP_END4_SETUP=1 is a test hook used by tests/smoke.sh to validate this
# script's own logic without running end-4's heavyweight installer.
echo "WARNING: the end-4 installer runs with --force and --skip-backup;"
echo "it may overwrite an existing Hyprland configuration without a backup."
if [ "${SKIP_END4_SETUP:-0}" = "1" ]; then
echo "(SKIP_END4_SETUP=1 - skipping the end-4 installer)"
else
git clone https://github.com/end-4/dots-hyprland.git
(
cd "$WORK_DIR/dots-hyprland"
./setup install --force --skip-plasmaintg --skip-backup --skip-quickshell --skip-hyprland --skip-hyprland-entry
)
fi
# Copy fish config files from end-4 to git repo if they don't exist
if [ ! -f configs/fish/config.fish ]; then
cp dots-hyprland/dots/.config/fish/config.fish configs/fish/config.fish
fi
# Resolve a config file: prefer this repo's copy, fall back to the end-4 clone
cfg_src() {
local rel="$1"
if [ -n "$SCRIPT_DIR" ] && [ -e "$SCRIPT_DIR/configs/$rel" ]; then
printf '%s\n' "$SCRIPT_DIR/configs/$rel"
elif [ -e "$WORK_DIR/dots-hyprland/dots/.config/$rel" ]; then
printf '%s\n' "$WORK_DIR/dots-hyprland/dots/.config/$rel"
else
return 1
fi
}
if [ ! -f configs/fish/auto-Hypr.fish ]; then
cp dots-hyprland/dots/.config/fish/auto-Hypr.fish configs/fish/auto-Hypr.fish
fi
if [ ! -f configs/fish/fish_variables ]; then
cp dots-hyprland/dots/.config/fish/fish_variables configs/fish/fish_variables
fi
# Copy color configs from end-4 to git repo if they don't exist
if [ ! -f configs/hypr/hyprland/colors.conf ]; then
cp dots-hyprland/dots/.config/hypr/hyprland/colors.conf configs/hypr/hyprland/colors.conf
fi
if [ ! -f configs/hypr/hyprlock/colors.conf ]; then
cp dots-hyprland/dots/.config/hypr/hyprlock/colors.conf configs/hypr/hyprlock/colors.conf
fi
# Run end-4 setup script with flags to skip unnecessary components
dots-hyprland/setup install --force --skip-plasmaintg --skip-backup --skip-quickshell --skip-hyprland --skip-hyprland-entry
# Create fish config directory
mkdir -p "$HOME/.config/fish"
# Copy a config file into $HOME if it does not exist yet
install_file() {
local rel="$1" dest="$2" src
if [ -e "$dest" ]; then
return 0
fi
if ! src="$(cfg_src "$rel")"; then
echo "warning: $rel not found in configs/ or the end-4 clone, skipping" >&2
return 0
fi
mkdir -p "$(dirname "$dest")"
cp "$src" "$dest"
}
# Copy fish config files to user config if they don't exist
if [ ! -f "$HOME/.config/fish/config.fish" ]; then
cp configs/fish/config.fish "$HOME/.config/fish/config.fish"
install_file fish/config.fish "$HOME/.config/fish/config.fish"
install_file fish/auto-Hypr.fish "$HOME/.config/fish/auto-Hypr.fish"
if cfg_src fish/functions >/dev/null 2>&1 && [ ! -d "$HOME/.config/fish/functions" ]; then
mkdir -p "$HOME/.config/fish"
cp -r "$(cfg_src fish/functions)" "$HOME/.config/fish/"
fi
if [ -d configs/fish/functions ] && [ ! -d "$HOME/.config/fish/functions" ]; then
cp -r configs/fish/functions "$HOME/.config/fish/"
if cfg_src fish/conf.d >/dev/null 2>&1 && [ ! -d "$HOME/.config/fish/conf.d" ]; then
mkdir -p "$HOME/.config/fish"
cp -r "$(cfg_src fish/conf.d)" "$HOME/.config/fish/"
fi
if [ -d configs/fish/conf.d ] && [ ! -d "$HOME/.config/fish/conf.d" ]; then
cp -r configs/fish/conf.d "$HOME/.config/fish/"
# Copy color configs to user config if they don't exist.
# end-4 renamed hyprland's colors.conf to colors.lua and keeps hyprlock's
# colors under hypr/hyprlock/ - warn and skip if upstream moves again.
install_file hypr/hyprland/colors.lua "$HOME/.config/hypr/hyprland/colors.lua"
install_file hypr/hyprlock/colors.conf "$HOME/.config/hypr/hyprlock/colors.conf"
# Install the Fisher plugin manager and nvm.fish, running inside fish (this is
# bash). The repo configs may already ship these functions (copied above) -
# fisher refuses to install over existing files, so only bootstrap what is
# actually missing instead of forcing a re-install.
if fish -c 'type -q fisher' 2>/dev/null; then
echo "Fisher is already available in fish (from configs/), skipping bootstrap."
else
if ! curl -fsSL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish \
-o "$WORK_DIR/fisher.fish"; then
echo "error: failed to download the fisher.fish installer" >&2
exit 1
fi
if ! fish -c "source '$WORK_DIR/fisher.fish' && fisher install jorgebucaran/fisher"; then
echo "error: failed to install Fisher into fish" >&2
exit 1
fi
fi
if [ ! -f "$HOME/.config/fish/auto-Hypr.fish" ]; then
cp configs/fish/auto-Hypr.fish "$HOME/.config/fish/"
if fish -c 'type -q nvm' 2>/dev/null; then
echo "nvm.fish is already available in fish, skipping install."
elif ! fish -c 'fisher install jorgebucaran/nvm.fish'; then
echo "error: failed to install nvm.fish into fish" >&2
exit 1
fi
# Copy color configs to user config if they don't exist
if [ ! -f "$HOME/.config/hypr/hyprland/colors.conf" ]; then
cp configs/hypr/hyprland/colors.conf "$HOME/.config/hypr/hyprland/colors.conf"
fi
if [ ! -f "$HOME/.config/hypr/hyprlock/colors.conf" ]; then
cp configs/hypr/hyprlock/colors.conf "$HOME/.config/hypr/hyprlock/colors.conf"
fi
# Install Fisher plugin manager for fish shell
curl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish | source && fisher install jorgebucaran/fisher
# Install plugins using Fisher
fisher install jorgebucaran/nvm.fish
echo "fish setup complete."
Executable
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Pre-push gate: syntax-check and shellcheck every tracked shell script.
# Enable once per clone with: git config core.hooksPath hooks
# Bypass a single push with: git push --no-verify
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
mapfile -t scripts < <(git ls-files '*.sh')
if [ "${#scripts[@]}" -eq 0 ]; then
echo "pre-push: no tracked shell scripts found" >&2
exit 0
fi
status=0
for f in "${scripts[@]}"; do
if ! bash -n "$f"; then
echo "pre-push: bash -n FAILED for $f" >&2
status=1
fi
done
if ! command -v shellcheck >/dev/null; then
echo "pre-push: shellcheck is not installed - the gate cannot run." >&2
echo " install it (e.g. 'pacman -S shellcheck' or 'apt install shellcheck')" >&2
echo " or bypass this once with: git push --no-verify" >&2
exit 1
fi
# SC1090 (can't follow non-constant source) is excluded: every 'source ~/.bashrc'
# style call in these scripts triggers it and it is not actionable.
if ! shellcheck -S warning -e SC1090 "${scripts[@]}"; then
echo "pre-push: shellcheck reported problems (see above)" >&2
status=1
fi
if [ "$status" -eq 0 ]; then
echo "pre-push: all ${#scripts[@]} shell scripts pass bash -n + shellcheck"
fi
exit "$status"
+44 -45
View File
@@ -8,9 +8,8 @@ fi
# Detect fish shell and warn user to run with bash instead (bash-only syntax)
if [ -n "${FISH_VERSION:-}" ] || case "$SHELL" in *fish*) ;; *) false ;; esac; then
if command -v whiptail >/dev/null; then
if ! whiptail --title "Warning: use bash, not fish" --yesno \
"Your default shell appears to be fish.\n\nThis script must be run with bash. It uses bash-only syntax and will fail under fish.\n\nRun it with:\n bash install.sh\n\nContinue anyway?" 15 72; then
if command -v gum >/dev/null; then
if ! gum confirm "$(printf 'Your default shell appears to be fish.\n\nThis script must be run with bash. It uses bash-only syntax and will fail under fish.\n\nRun it with:\n bash install.sh\n\nContinue anyway?')"; then
echo "Aborted. Please run this script with bash, not fish. e.g.: bash install.sh"
exit 1
fi
@@ -84,70 +83,70 @@ fi
# Install prerequisites
if command -v shelly >/dev/null; then
echo "Shelly detected. Installing prerequisites"
shelly install standard git zsh curl wget libnewt rsync whiptail --upgrade --no-confirm
shelly install standard git zsh curl wget libnewt rsync gum --upgrade --no-confirm
fi
if command -v apt >/dev/null; then
echo "apt detected. Installing prerequisites"
sudo apt update
sudo apt full-upgrade -y
sudo apt install git zsh curl wget whiptail rsync -y
sudo apt install git zsh curl wget rsync gum -y
fi
# Ensure whiptail is installed
if ! command -v whiptail >/dev/null; then
echo "whiptail is not installed. Installing it now..."
# Ensure gum is installed
if ! command -v gum >/dev/null; then
echo "gum is not installed. Installing it now..."
if command -v shelly >/dev/null; then
echo "shelly detected. Installing whiptail"
shelly install standard whiptail --no-confirm || {
echo "Failed to install whiptail. Exiting."
echo "shelly detected. Installing gum"
shelly install standard gum --no-confirm || {
echo "Failed to install gum. Exiting."
exit 1
}
fi
if command -v apt >/dev/null; then
echo "apt detected. Installing whiptail"
sudo apt install whiptail -y || {
echo "Failed to install whiptail. Exiting."
echo "apt detected. Installing gum"
sudo apt install gum -y || {
echo "Failed to install gum. Exiting."
exit 1
}
fi
fi
# Options for the whiptail menu
# Options for the gum menu
OPTIONS=(
1 "Run zsh setup script" OFF
2 "Run fish setup script" ON
3 "Run LazyVim setup script" OFF
4 "Install Docker" OFF
5 "Install Standard Packages (Shelly)" ON
6 "Install AUR Packages (Shelly)" ON
7 "Install affinity with GUI" OFF
8 "Run bat setup script" ON
"Run zsh setup script"
"Run fish setup script"
"Run LazyVim setup script"
"Install Docker"
"Install Standard Packages (Shelly)"
"Install AUR Packages (Shelly)"
"Install affinity with GUI"
"Run bat setup script"
)
CHOICE=$(whiptail --title "Installation Options" --checklist \
"Choose components to install:" 20 78 10 \
"${OPTIONS[@]}" 3>&1 1>&2 2>&3)
exitstatus=$?
if [ $exitstatus = 0 ]; then
echo "User selected: $CHOICE"
else
CHOICE=$(gum choose --no-limit --height 14 \
--header "Tab = select, Enter = confirm:" \
--selected "Run fish setup script,Install Standard Packages (Shelly),Install AUR Packages (Shelly),Run bat setup script" \
"${OPTIONS[@]}") || {
echo "User cancelled installation."
exit 1
fi
}
echo "User selected:"
echo "$CHOICE"
# Fail on any command.
set -euo pipefail
# Process selected options
for selection in $CHOICE; do
clean_selection=$(echo "$selection" | tr -d '"')
case $clean_selection in
"1")
# Process selected options in menu order
while IFS= read -r selection; do
# Skip options the user did not select
grep -qxF "$selection" <<<"$CHOICE" || continue
case "$selection" in
"Run zsh setup script")
echo "Running zsh setup script..."
# Runs local script unless it does not exist or fails
if [[ -f "zsh.sh" ]]; then
@@ -157,7 +156,7 @@ for selection in $CHOICE; do
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/zsh.sh | bash -s -- --skip-watermark
fi
;;
"2")
"Run fish setup script")
echo "Running fish setup script..."
# Runs local script unless it does not exist or fails
if [[ -f "fish.sh" ]]; then
@@ -167,7 +166,7 @@ for selection in $CHOICE; do
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/fish.sh | bash -s -- --skip-watermark
fi
;;
"3")
"Run LazyVim setup script")
echo "Running LazyVim setup script..."
# Runs local script unless it does not exist or fails
if [[ -f "LazyVim.sh" ]]; then
@@ -177,7 +176,7 @@ for selection in $CHOICE; do
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/LazyVim.sh | bash
fi
;;
"4")
"Install Docker")
echo "Installing Docker..."
if ! command -v docker >/dev/null; then
echo "docker is NOT installed. Installing..."
@@ -189,7 +188,7 @@ for selection in $CHOICE; do
echo "+ sleep 10" && sleep 10
fi
;;
"5")
"Install Standard Packages (Shelly)")
echo "Installing Standard Packages via Shelly backup..."
# Check if shelly binary is installed
if ! command -v shelly >/dev/null; then
@@ -215,7 +214,7 @@ for selection in $CHOICE; do
echo "+ sleep 10" && sleep 10
fi
;;
"6")
"Install AUR Packages (Shelly)")
echo "Installing AUR Packages via Shelly backup..."
# Check for shelly before installing AUR packages
if ! command -v shelly >/dev/null; then
@@ -241,7 +240,7 @@ for selection in $CHOICE; do
echo "+ sleep 10" && sleep 10
fi
;;
"7")
"Install affinity with GUI")
echo "Installing GUI dependencies"
if command -v shelly >/dev/null; then
echo "shelly found"
@@ -262,7 +261,7 @@ for selection in $CHOICE; do
sleep 10
curl -sSL https://raw.githubusercontent.com/ryzendew/AffinityOnLinux/refs/heads/main/AffinityScripts/AffinityLinuxInstaller.py | python3
;;
"8")
"Run bat setup script")
echo "Running bat setup script..."
# Runs local script unless it does not exist or fails
if [[ -f "bat.sh" ]]; then
@@ -277,6 +276,6 @@ for selection in $CHOICE; do
echo "Invalid option selected: $selection"
;;
esac
done
done < <(printf '%s\n' "${OPTIONS[@]}")
echo "Installation process complete."
+419 -162
View File
@@ -1,29 +1,17 @@
#!/usr/bin/bash
PreviousWD=$(pwd)
# Strict mode from the very start: every tolerated failure in this script is
# an explicit guard (if ! / || record_failure), so preamble steps are checked
# too. Anything unguarded aborts the run via errexit.
set -euo pipefail
skip_watermark=false
if [ "$1" = "--skip-watermark" ]; then
if [ "${1:-}" = "--skip-watermark" ]; then
skip_watermark=true
fi
# Detect fish shell and warn user to run with bash instead (bash-only syntax)
if [ -n "${FISH_VERSION:-}" ] || case "$SHELL" in *fish*) ;; *) false ;; esac; then
if command -v whiptail >/dev/null; then
if ! whiptail --title "Warning: use bash, not fish" --yesno \
"Your default shell appears to be fish.\n\nThis script must be run with bash. It uses bash-only syntax and will fail under fish.\n\nRun it with:\n bash installbeta.sh\n\nContinue anyway?" 15 72; then
echo "Aborted. Please run this script with bash, not fish. e.g.: bash installbeta.sh"
exit 1
fi
else
echo "Warning: fish shell detected."
echo "This script must be run with bash, not fish (bash-only syntax)."
echo "Please run it with bash, e.g.: bash installbeta.sh"
read -r -p "Press Enter to continue anyway, or Ctrl+C to cancel..." </dev/tty || true
fi
fi
if [ "$skip_watermark" = false ]; then
clear
clear 2>/dev/null || true
echo '
____ _ _ _ _ _
| __ ) _ _ __ _| |_ _(_)_ __ | | ___ | | | ___
@@ -33,49 +21,210 @@ if [ "$skip_watermark" = false ]; then
|___/
--------------- Single Download script ---------------
BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE UNDER THE GPL-2.0 LICENCE,
BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE UNDER THE GPL-2.0 LICENSE,
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
See the LICENCE for more detail
See the LICENSE for more detail
'
fi
# Function to display error message
function error_handler() {
set +x
echo -e "An error occurred. Please check the output above for details."
# --- Failure handling & full console log ------------------------------------
# Everything from here on is teed to a log file in /tmp, and Arch-only step
# failures are recorded instead of aborting the rest of the installation.
TTY_OK=false
if [ -t 1 ]; then
TTY_OK=true
fi
# One timestamp for both files, mktemp so the names are not predictable
STAMP="$(date +%Y%m%d-%H%M%S)"
LOG="$(mktemp "/tmp/installbeta-$STAMP-XXXXXX.log")"
REPORT="$(mktemp "/tmp/installbeta-failures-$STAMP-XXXXXX.txt")"
FAILED=()
exec > >(tee -a "$LOG") 2>&1
# gum draws its TUI on stderr - keep that on the real terminal, not the log pipe
gum_tty() {
if [ "$TTY_OK" = true ]; then
gum "$@" 2>/dev/tty
else
gum "$@"
fi
}
# Trap errors
trap error_handler ERR
# Run a long non-interactive step behind a spinner when gum exists, plain
# otherwise: the shelly bootstrap below must work on a system that has no gum
# yet. Routed through gum_tty so spinner escape codes stay out of $LOG.
run_step() {
local title="$1" out rc=0
shift
if ! command -v gum >/dev/null; then
"$@"
return
fi
# gum spin --show-error would print the step's errors on gum's stderr, which
# gum_tty sends to the terminal only. Capture the output here instead and
# replay it on failure through stdout, so the cause also lands in $LOG.
out="$(mktemp "$WORK_DIR/step-XXXXXX.out")"
gum_tty spin -s dot --title "$title" -- bash -c '"$@" >"$0" 2>&1' "$out" "$@" || rc=$?
if [ "$rc" -ne 0 ]; then
cat "$out"
fi
rm -f "$out"
return "$rc"
}
# Install shelly as a dependency if not present (Arch-based)
# Errors through gum log (level + colour + timestamp) once gum exists; plain
# echo before that, since the first record_failure can precede the bootstrap.
log_error() {
if command -v gum >/dev/null; then
gum log --level error --time kitchen -- "$*"
else
echo "!! $*"
fi
}
# Same, for the failures that abort the run. `|| true` because gum exits 1
# on level fatal, which errexit would otherwise trip over mid-message.
log_fatal() {
if command -v gum >/dev/null; then
gum log --level fatal --time kitchen -- "$*" || true
else
echo "FATAL: $*"
fi
}
# Record a failed step: remember it, print it now, keep installing
record_failure() {
FAILED+=("$1 — $2")
log_error "$1 — $2 (continuing with the rest of the installation)"
# Plain line, no colour codes: this is what gum pager shows at the end
echo "$(date '+%F %T') $1 — $2" >>"$REPORT"
}
# Run a command; if it fails, print that exact command as a retry hint and
# record the failure. The hint is built from the arguments, so it cannot drift
# from what actually ran. Pass --spin TITLE first to run it via run_step.
# Usage: try_step STEP REASON MESSAGE [--spin TITLE] CMD...
try_step() {
local step="$1" reason="$2" message="$3" title="" hint
shift 3
if [ "$1" = "--spin" ]; then
title="$2"
shift 2
fi
if [ -n "$title" ]; then
run_step "$title" "$@" && return 0
else
"$@" && return 0
fi
printf -v hint '%q ' "$@"
echo "--------------------------------------------------------------------"
log_error "$message You can try running it manually:"
echo "${hint% }"
echo "--------------------------------------------------------------------"
record_failure "$step" "$reason"
return 1
}
# Installed-package count for the final summary. apt is deliberately not
# reported: on apt-only systems this prints nothing and no count line shows.
snapshot_pkg_count() {
if command -v pacman >/dev/null; then
pacman -Qq 2>/dev/null | wc -l
elif command -v rpm >/dev/null; then
rpm -qa --qf '%{NAME}\n' 2>/dev/null | wc -l
fi
}
# Private temp workspace for downloads and clones. Predictable names in /tmp
# are a symlink attack vector on multi-user systems; the trap cleans up too.
WORK_DIR="$(mktemp -d)"
# Also stops the sudo keep-alive loop below when the script exits (any path:
# normal end, menu cancel, errexit, signal). SUDO_LOOP_PID is unset if the
# script bails out before the loop starts, hence the default value.
cleanup() {
if [ -n "${SUDO_LOOP_PID:-}" ]; then
# The loop's TERM trap also stops its current `sleep` (see below)
kill "$SUDO_LOOP_PID" 2>/dev/null || true
fi
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
# Resolve where this script lives so local script/config fallbacks do not
# depend on the caller's current directory. Empty when piped (curl | bash).
if [ -n "${BASH_SOURCE[0]:-}" ] && [ -f "${BASH_SOURCE[0]}" ]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
else
SCRIPT_DIR=""
fi
# Ask for sudo upfront so the user is prompted only once. A cancelled prompt or
# a system without sudo aborts here with a clear message instead of an
# unexplained errexit exit later on.
if ! sudo -v; then
log_fatal "sudo is required to run this installer (password prompt cancelled, or sudo is not available)."
exit 1
fi
# Sudo keep-alive loop
sudo_keep_alive() {
local sleep_pid=""
# On TERM (from cleanup) stop the current sleep before exiting, otherwise it
# lingers orphaned for up to 250s. Sleeping in the background and waiting
# lets the trap fire immediately, and nothing else runs after it.
trap 'kill "$sleep_pid" 2>/dev/null; exit' TERM
while true; do
sleep 250 &
sleep_pid=$!
wait "$sleep_pid"
sudo -n true 2>/dev/null || true
# $$ is the main script's PID even in this background subshell: exit as
# soon as the installer is gone, as a backstop for the EXIT trap.
kill -0 "$$" 2>/dev/null || exit
done
}
# stderr silenced: no "Terminated" report from the loop's killed `sleep` may
# land in the tee'd log.
sudo_keep_alive 2>/dev/null &
SUDO_LOOP_PID=$!
echo "Keeping sudo alive on PID $SUDO_LOOP_PID - refreshing every 250s"
# Baseline for the package count in the final summary (empty on apt-only
# systems, where no count is reported). Taken before the first install step,
# which is the shelly bootstrap right below.
PKGS_BEFORE="$(snapshot_pkg_count)"
# Install shelly as a dependency if not present (Arch-based). This runs before
# any gum use on purpose: on a fresh Arch box gum comes from pacman here, so
# the bootstrap never depends on gum already being installed (and gum itself
# cannot be installed without shelly or apt).
if command -v pacman >/dev/null; then
# Install build prerequisites for shelly
if ! sudo pacman -S --needed --noconfirm base-devel git; then
echo "--------------------------------------------------------------------"
echo "Failed to install prerequisite packages for Shelly. You can try running it manually:"
echo "sudo pacman -S --needed --noconfirm base-devel git"
echo "--------------------------------------------------------------------"
exit 1 # exit with an error
# Install build prerequisites for shelly. Plain command, not run_step: gum
# may not exist yet at this point.
try_step "Shelly bootstrap (Arch-only)" "failed to install base-devel/git prerequisites" \
"Failed to install prerequisite packages for Shelly." \
sudo pacman -S --needed --noconfirm base-devel git || true
# gum from [extra], so the shelly build below gets a spinner and the rest of
# the script has gum even if that build fails. Its own transaction and
# non-fatal: a repo set without gum, or a stale sync db pointing at a removed
# version, must not take base-devel/git down with it. No -Sy here (that
# would be a partial upgrade); the safety net below retries via shelly.
if ! command -v gum >/dev/null && ! sudo pacman -S --needed --noconfirm gum; then
echo "Could not install gum with pacman - retrying via shelly after the bootstrap."
fi
# Check if shelly binary exists
if ! command -v shelly >/dev/null; then
echo "shelly is NOT installed. Running installation commands..."
if ! git clone https://aur.archlinux.org/shelly.git /tmp/shelly_install; then
echo "--------------------------------------------------------------------"
echo "Failed to clone shelly repository. You can try running it manually:"
echo "git clone https://aur.archlinux.org/shelly.git /tmp/shelly_install"
echo "--------------------------------------------------------------------"
exit 1 # exit with an error
elif ! (cd /tmp/shelly_install && makepkg -si --noconfirm); then
echo "--------------------------------------------------------------------"
echo "Failed to build and install shelly. You can try running it manually:"
echo "cd /tmp/shelly_install && makepkg -si --noconfirm"
echo "--------------------------------------------------------------------"
exit 1 # exit with an error
if try_step "Shelly bootstrap (Arch-only)" "failed to clone the shelly AUR repository" \
"Failed to clone shelly repository." --spin "Clone Shelly from AUR" \
git clone https://aur.archlinux.org/shelly.git "$WORK_DIR/shelly"; then
try_step "Shelly bootstrap (Arch-only)" "failed to build shelly with makepkg" \
"Failed to build and install shelly." --spin "Install Shelly" \
makepkg -D "$WORK_DIR/shelly" -si --noconfirm || true
fi
cd "$PreviousWD"
rm -rf /tmp/shelly_install
else
echo "Shelly is already installed."
fi
@@ -84,198 +233,306 @@ fi
# Install prerequisites
if command -v shelly >/dev/null; then
echo "Shelly detected. Installing prerequisites"
shelly install standard git zsh curl wget libnewt rsync whiptail --upgrade --no-confirm
# No gum here: pacman above provides it, the safety net below is the fallback
shelly install standard base-devel git zsh curl wget libnewt rsync --upgrade --no-confirm ||
record_failure "Prerequisites (shelly)" "installing base packages via shelly failed"
fi
if command -v apt >/dev/null; then
echo "apt detected. Installing prerequisites"
sudo apt update
sudo apt full-upgrade -y
sudo apt install git zsh curl wget whiptail rsync -y
sudo apt update || record_failure "Prerequisites (apt)" "apt update failed"
sudo apt full-upgrade -y || record_failure "Prerequisites (apt)" "apt full-upgrade failed"
sudo -v
sudo apt install git zsh curl wget rsync gum -y ||
record_failure "Prerequisites (apt)" "apt install of base packages failed"
fi
# Ensure whiptail is installed
if ! command -v whiptail >/dev/null; then
echo "whiptail is not installed. Installing it now..."
# Ensure gum is installed (safety net: pacman/apt above should already have
# provided it, but nothing below can run without it)
if ! command -v gum >/dev/null; then
echo "gum is not installed. Installing it now..."
if command -v shelly >/dev/null; then
echo "shelly detected. Installing whiptail"
shelly install standard whiptail --no-confirm || {
echo "Failed to install whiptail. Exiting."
echo "shelly detected. Installing gum"
shelly install standard gum --no-confirm || {
log_fatal "Failed to install gum. Exiting."
exit 1
}
fi
if command -v apt >/dev/null; then
echo "apt detected. Installing whiptail"
sudo apt install whiptail -y || {
echo "Failed to install whiptail. Exiting."
echo "apt detected. Installing gum"
sudo apt install gum -y || {
log_fatal "Failed to install gum. Exiting."
exit 1
}
fi
fi
# Options for the whiptail menu
OPTIONS=(
1 "Run zsh setup script" OFF
2 "Run fish setup script" ON
3 "Run LazyVim setup script" OFF
4 "Install Docker" OFF
5 "Install Standard Packages (Shelly)" ON
6 "Install AUR Packages (Shelly)" ON
7 "Install affinity with GUI" OFF
8 "Run bat setup script" ON
)
CHOICE=$(whiptail --title "Installation Options" --checklist \
"Choose components to install:" 20 78 10 \
"${OPTIONS[@]}" 3>&1 1>&2 2>&3)
exitstatus=$?
if [ $exitstatus = 0 ]; then
echo "User selected: $CHOICE"
else
echo "User cancelled installation."
# gum drives every interactive dialog in this script and there is no fallback
# menu, so a system that ended up without it must abort here.
if ! command -v gum >/dev/null; then
log_fatal "gum is required but could not be installed on this system (no shelly/apt found)."
echo "Install it manually: https://github.com/charmbracelet/gum" >&2
exit 1
fi
# Fail on any command.
set -euo pipefail
# Detect fish shell and warn user to run with bash instead (bash-only syntax).
# Placed after the gum bootstrap above so the dialog can always use gum confirm
# instead of falling back to a plain text prompt.
if [ -n "${FISH_VERSION:-}" ] || case "${SHELL:-}" in *fish*) ;; *) false ;; esac then
if ! gum_tty confirm "$(printf 'Your default shell appears to be fish.\n\nThis script must be run with bash. It uses bash-only syntax and will fail under fish.\n\nRun it with:\n bash installbeta.sh\n\nContinue anyway?')"; then
log_fatal "Aborted. Please run this script with bash, not fish. e.g.: bash installbeta.sh"
exit 1
fi
fi
# Process selected options
for selection in $CHOICE; do
clean_selection=$(echo "$selection" | tr -d '"')
case $clean_selection in
"1")
# Options for the gum menu
OPTIONS=(
"Run zsh setup script"
"Run fish setup script"
"Run LazyVim setup script"
"Install Docker"
"Install Standard Packages (Shelly)"
"Install AUR Packages (Shelly)"
"Install affinity with GUI"
"Run bat setup script"
)
# Present the menu with gum (gum is guaranteed by the bootstrap above)
if ! CHOICE=$(gum_tty choose --no-limit --height 14 \
--header "Tab = select, Enter = confirm:" \
--selected "Run fish setup script,Install Standard Packages (Shelly),Install AUR Packages (Shelly),Run bat setup script" \
"${OPTIONS[@]}"); then
echo "No options selected (menu cancelled). Nothing was installed."
exit 1
fi
# gum exits 0 with empty output when nothing is selected - treat that as a
# cancel too, otherwise the run below would silently install nothing.
if [ -z "$CHOICE" ]; then
echo "No options selected. Nothing was installed."
exit 1
fi
echo "User selected:"
echo "$CHOICE"
# Process selected options in menu order
while IFS= read -r selection; do
# Skip options the user did not select
grep -qxF "$selection" <<<"$CHOICE" || continue
case "$selection" in
"Run zsh setup script")
echo "Running zsh setup script..."
# Runs local script unless it does not exist or fails
if [[ -f "zsh.sh" ]]; then
sudo -v
# Runs local script unless it does not exist (SCRIPT_DIR, not CWD)
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/zsh.sh" ]; then
echo "Found local script, running..."
bash zsh.sh --skip-watermark
bash "$SCRIPT_DIR/zsh.sh" --skip-watermark || {
rc=$?
record_failure "Run zsh setup script" "zsh.sh exited with status $rc"
}
else
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/zsh.sh | bash -s -- --skip-watermark
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/zsh.sh | bash -s -- --skip-watermark || {
rc=$?
record_failure "Run zsh setup script" "downloaded zsh.sh exited with status $rc"
}
fi
;;
"2")
"Run fish setup script")
echo "Running fish setup script..."
# Runs local script unless it does not exist or fails
if [[ -f "fish.sh" ]]; then
sudo -v
# Runs local script unless it does not exist (SCRIPT_DIR, not CWD)
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/fish.sh" ]; then
echo "Found local script, running..."
bash fish.sh --skip-watermark
bash "$SCRIPT_DIR/fish.sh" --skip-watermark || {
rc=$?
record_failure "Run fish setup script" "fish.sh exited with status $rc"
}
else
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/fish.sh | bash -s -- --skip-watermark
# Top-level pipe (like the zsh branch) so set -o pipefail catches a
# failed download instead of reporting an empty script as success.
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/fish.sh | bash -s -- --skip-watermark || {
rc=$?
record_failure "Run fish setup script" "downloaded fish.sh exited with status $rc"
}
fi
;;
"3")
"Run LazyVim setup script")
echo "Running LazyVim setup script..."
# Runs local script unless it does not exist or fails
if [[ -f "LazyVim.sh" ]]; then
sudo -v
# Runs local script unless it does not exist (SCRIPT_DIR, not CWD)
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/LazyVim.sh" ]; then
echo "Found local script, running..."
bash LazyVim.sh
bash "$SCRIPT_DIR/LazyVim.sh" || {
rc=$?
record_failure "Run LazyVim setup script" "LazyVim.sh exited with status $rc"
}
else
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/LazyVim.sh | bash
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/LazyVim.sh | bash || {
rc=$?
record_failure "Run LazyVim setup script" "downloaded LazyVim.sh exited with status $rc"
}
fi
;;
"4")
"Install Docker")
echo "Installing Docker..."
sudo -v
if ! command -v docker >/dev/null; then
echo "docker is NOT installed. Installing..."
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker "$USER"
if curl -fsSL https://get.docker.com -o "$WORK_DIR/get-docker.sh" &&
sh "$WORK_DIR/get-docker.sh"; then
sudo usermod -aG docker "$USER" ||
record_failure "Install Docker" "adding $USER to the docker group failed"
else
sudo usermod -aG docker "$USER"
echo "Docker is already installed."
echo "+ sleep 10" && sleep 10
rc=$?
record_failure "Install Docker" "docker installer script exited with status $rc"
fi
else
sudo usermod -aG docker "$USER" ||
record_failure "Install Docker" "adding $USER to the docker group failed"
echo "Docker is already installed."
fi
echo "NOTE: $USER has been added to the docker group."
echo "Log out and back in (or reboot) for the group membership to take effect."
;;
"5")
"Install Standard Packages (Shelly)")
echo "Installing Standard Packages via Shelly backup..."
sudo -v
# Check if shelly binary is installed
if ! command -v shelly >/dev/null; then
echo "Cannot proceed: shelly binary not found"
exit 1 # exit with an error
fi
echo "Cannot proceed: shelly binary not found (Arch-only)"
record_failure "Install Standard Packages (Shelly)" "shelly binary not found (Arch-only)"
# Install standard packages from shelly backup
# Runs local script unless it does not exist or fails
if [[ -f "./configs/shelly-standard.toml" ]]; then
# Runs local script unless it does not exist (SCRIPT_DIR, not CWD)
elif [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/configs/shelly-standard.toml" ]; then
echo "Found local backup"
shelly backup --import --name shelly-standard --directory ./configs --no-confirm
elif ! curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/configs/shelly-standard.toml -o /tmp/shelly-standard.toml; then
echo "--------------------------------------------------------------------"
echo "Failed to download shelly backup. You can try running it manually:"
echo "curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/configs/shelly-standard.toml -o /tmp/shelly-standard.toml"
echo "--------------------------------------------------------------------"
echo "+ sleep 10" && sleep 10
elif ! shelly backup --import --name shelly-standard --directory /tmp --no-confirm; then
echo "--------------------------------------------------------------------"
echo "Failed to install Standard packages. You can try running it manually:"
echo "shelly backup --import --name shelly-standard --directory /tmp --no-confirm"
echo "--------------------------------------------------------------------"
echo "+ sleep 10" && sleep 10
shelly backup --import --name shelly-standard --directory "$SCRIPT_DIR/configs" --no-confirm || {
rc=$?
record_failure "Install Standard Packages (Shelly)" "shelly backup import exited with status $rc"
}
elif try_step "Install Standard Packages (Shelly)" "failed to download shelly-standard.toml" \
"Failed to download shelly backup." \
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/configs/shelly-standard.toml -o "$WORK_DIR/shelly-standard.toml"; then
try_step "Install Standard Packages (Shelly)" "shelly backup import failed" \
"Failed to install Standard packages." \
shelly backup --import --name shelly-standard --directory "$WORK_DIR" --no-confirm || true
fi
;;
"6")
"Install AUR Packages (Shelly)")
echo "Installing AUR Packages via Shelly backup..."
sudo -v
# Check for shelly before installing AUR packages
if ! command -v shelly >/dev/null; then
echo "Cannot proceed: shelly binary not found"
exit 1 # Exit with an error
fi
echo "Cannot proceed: shelly binary not found (Arch-only)"
record_failure "Install AUR Packages (Shelly)" "shelly binary not found (Arch-only)"
# Install AUR packages from shelly backup
# Runs local script unless it does not exist or fails
if [[ -f "./configs/shelly-aur.toml" ]]; then
# Runs local script unless it does not exist (SCRIPT_DIR, not CWD)
elif [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/configs/shelly-aur.toml" ]; then
echo "Found local backup"
shelly backup --import --name shelly-aur --directory ./configs --no-confirm
elif ! curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/configs/shelly-aur.toml -o /tmp/shelly-aur.toml; then
echo "--------------------------------------------------------------------"
echo "Failed to download shelly backup. You can try running it manually:"
echo "curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/configs/shelly-aur.toml -o /tmp/shelly-aur.toml"
echo "--------------------------------------------------------------------"
echo "+ sleep 10" && sleep 10
elif ! shelly backup --import --name shelly-aur --directory /tmp --no-confirm; then
echo "--------------------------------------------------------------------"
echo "Failed to install AUR packages. You can try running it manually:"
echo "shelly backup --import --name shelly-aur --directory /tmp --no-confirm"
echo "--------------------------------------------------------------------"
echo "+ sleep 10" && sleep 10
shelly backup --import --name shelly-aur --directory "$SCRIPT_DIR/configs" --no-confirm || {
rc=$?
record_failure "Install AUR Packages (Shelly)" "shelly backup import exited with status $rc"
}
elif try_step "Install AUR Packages (Shelly)" "failed to download shelly-aur.toml" \
"Failed to download shelly backup." \
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/configs/shelly-aur.toml -o "$WORK_DIR/shelly-aur.toml"; then
try_step "Install AUR Packages (Shelly)" "shelly backup import failed" \
"Failed to install AUR packages." \
shelly backup --import --name shelly-aur --directory "$WORK_DIR" --no-confirm || true
fi
;;
"7")
"Install affinity with GUI")
echo "Installing GUI dependencies"
sudo -v
if command -v shelly >/dev/null; then
echo "shelly found"
shelly install standard python-pyqt6 --no-confirm
shelly install standard python-pyqt6 --no-confirm ||
record_failure "Install affinity with GUI" "installing python-pyqt6 via shelly failed"
fi
if command -v dnf >/dev/null; then
echo "dnf found"
sudo dnf install python3-pyqt6
sudo dnf install python3-pyqt6 ||
record_failure "Install affinity with GUI" "installing python3-pyqt6 via dnf failed"
fi
if command -v apt >/dev/null; then
echo "apt found"
sudo apt install python3-pyqt6 -y
sudo apt install python3-pyqt6 -y ||
record_failure "Install affinity with GUI" "installing python3-pyqt6 via apt failed"
fi
echo "Install affinity with ryzendew's gui installer"
echo "You must manually select to install in the GUI"
echo "Github repo: https://github.com/ryzendew/Linux-Affinity-Installer"
echo "+ 10 sleep"
echo "Starting the GUI installer in 10 seconds - read the notes above first."
sleep 10
curl -sSL https://raw.githubusercontent.com/ryzendew/AffinityOnLinux/refs/heads/main/AffinityScripts/AffinityLinuxInstaller.py | python3
{ curl -fsSL https://raw.githubusercontent.com/ryzendew/AffinityOnLinux/refs/heads/main/AffinityScripts/AffinityLinuxInstaller.py \
-o "$WORK_DIR/AffinityLinuxInstaller.py" &&
python3 "$WORK_DIR/AffinityLinuxInstaller.py"; } || {
rc=$?
record_failure "Install affinity with GUI" "affinity installer exited with status $rc"
}
;;
"8")
"Run bat setup script")
echo "Running bat setup script..."
# Runs local script unless it does not exist or fails
if [[ -f "bat.sh" ]]; then
# Runs local script unless it does not exist (SCRIPT_DIR, not CWD)
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/bat.sh" ]; then
echo "Found local script, running..."
bash bat.sh --skip-watermark
bash "$SCRIPT_DIR/bat.sh" --skip-watermark || {
rc=$?
record_failure "Run bat setup script" "bat.sh exited with status $rc"
}
else
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/bat.sh | bash -s -- --skip-watermark
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/bat.sh | bash -s -- --skip-watermark || {
rc=$?
record_failure "Run bat setup script" "downloaded bat.sh exited with status $rc"
}
fi
;;
*)
echo "Invalid option selected: $selection"
log_error "Invalid option selected: $selection"
;;
esac
done
done < <(printf '%s\n' "${OPTIONS[@]}")
echo "Installation process complete."
# Show (and save) what failed - the run itself never aborts on a failed step
if [ "${#FAILED[@]}" -gt 0 ]; then
{
echo ""
echo "================ FAILURE SUMMARY ================"
echo "installbeta.sh finished at $(date '+%F %T') - ${#FAILED[@]} step(s) failed:"
printf ' - %s\n' "${FAILED[@]}"
echo ""
echo "Full console log: $LOG"
echo "Failure report: $REPORT"
echo "==================================================="
} | tee -a "$REPORT"
if [ "$TTY_OK" = true ] && command -v gum >/dev/null; then
gum_tty pager <"$REPORT" || true
fi
# Non-zero so callers and CI can detect the failed run
exit 1
fi
echo "================ INSTALL SUMMARY ================"
echo "installbeta.sh finished at $(date '+%F %T') - no failures."
if [ -n "$PKGS_BEFORE" ]; then
PKG_DELTA=$(($(snapshot_pkg_count) - PKGS_BEFORE))
if [ "$PKG_DELTA" -gt 0 ]; then
echo "Packages added: $PKG_DELTA"
elif [ "$PKG_DELTA" -lt 0 ]; then
echo "Packages removed: $((-PKG_DELTA))"
else
echo "Packages added: 0 (no net change)"
fi
fi
echo ""
echo "Full console log: $LOG"
echo "Failure report: $REPORT"
echo "==================================================="
exit 0
Executable
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env bash
# Manual end-to-end smoke test. NOT part of the pre-push hook - run by hand:
#
# tests/smoke.sh # default set: bat.sh zsh.sh fish.sh
# tests/smoke.sh LazyVim.sh # opt in to the heavy LazyVim run
# tests/smoke.sh bat.sh # run specific scripts only
#
# Runs each script inside a throwaway Arch container with a stubbed 'sudo'
# (the container is already root). Needs docker or podman and network access.
# This is what catches the bug class shellcheck cannot: inverted conditions,
# swapped shell branches, quoting that collapses when written to a file.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if command -v docker >/dev/null 2>&1; then
RUNTIME=docker
elif command -v podman >/dev/null 2>&1; then
RUNTIME=podman
else
echo "error: neither docker nor podman found - cannot run the smoke test." >&2
exit 1
fi
DEFAULT_SCRIPTS=(bat.sh zsh.sh fish.sh)
if [ "$#" -gt 0 ]; then
SCRIPTS=("$@")
else
SCRIPTS=("${DEFAULT_SCRIPTS[@]}")
fi
# Validate arguments up front so a typo does not run in a container first
for s in "${SCRIPTS[@]}"; do
if [ ! -f "$ROOT/$s" ]; then
echo "error: $s not found in $ROOT" >&2
exit 1
fi
done
# Everything inside the container:
# - copy the read-only mount so scripts that write to their own directory
# (fish.sh syncing configs/) cannot mutate the host checkout
# - stub 'sudo' -> exec (container already runs as root)
# - pacman -Sy so package lookups work on a fresh image
# - HOME=/root keeps every script writing inside the container
container_run() {
local body="$1"
"$RUNTIME" run --rm \
-v "$ROOT:/src:ro" \
-e HOME=/root \
archlinux:latest \
bash -c "
set -euo pipefail
cp -r /src /work
cd /work
printf '#!/bin/sh\nexec \"\$@\"\n' > /usr/local/bin/sudo
chmod +x /usr/local/bin/sudo
pacman -Sy --noconfirm >/dev/null 2>&1 || true
$body
"
}
declare -a RESULTS=()
overall=0
run_test() {
local script="$1" body="$2"
echo ""
echo "=== smoke: $script ==="
if container_run "$body"; then
echo "=== PASS: $script ==="
RESULTS+=("PASS $script")
else
echo "=== FAIL: $script (exit $?)" >&2
RESULTS+=("FAIL $script")
overall=1
fi
}
for script in "${SCRIPTS[@]}"; do
case "$script" in
bat.sh)
run_test "$script" '
bash bat.sh --skip-watermark
command -v bat >/dev/null || { echo "bat not on PATH after install"; exit 1; }
grep -qxF -- "--theme=\"Catppuccin Mocha\"" "$(bat --config-dir)/config" \
|| { echo "theme line missing from bat config:"; cat "$(bat --config-dir)/config" 2>/dev/null; exit 1; }
[ -s "$(bat --config-dir)/themes/Catppuccin Mocha.tmTheme" ] \
|| { echo "theme file was not downloaded"; exit 1; }
# re-run must be idempotent (no duplicate config lines)
bash bat.sh --skip-watermark
[ "$(grep -cF -- "--theme=" "$(bat --config-dir)/config")" -eq 1 ] \
|| { echo "bat config is not idempotent"; exit 1; }
'
;;
zsh.sh)
run_test "$script" '
# pre-seed a config so BOTH runs have something to back up
printf "# preexisting zshrc marker\n" > /root/.zshrc
bash zsh.sh --skip-watermark
[ -f /root/.zshrc ] || { echo ".zshrc missing"; exit 1; }
[ -d /root/.oh-my-zsh/custom/plugins/zsh-autosuggestions ] || { echo "plugins missing"; exit 1; }
n=$(find /root -maxdepth 1 -name ".zshrc.bak-*" | wc -l)
[ "$n" -eq 1 ] || { echo "expected 1 backup after first run, got $n"; exit 1; }
grep -q "preexisting zshrc marker" /root/.zshrc.bak-* \
|| { echo "first backup does not hold the original config"; exit 1; }
# second run must rotate (new timestamped backup), not overwrite
bash zsh.sh --skip-watermark
n=$(find /root -maxdepth 1 -name ".zshrc.bak-*" | wc -l)
[ "$n" -eq 2 ] || { echo "expected 2 backups after second run (rotation), got $n"; exit 1; }
grep -q "preexisting zshrc marker" /root/.zshrc.bak-* \
|| { echo "second run destroyed an earlier backup"; exit 1; }
'
;;
fish.sh)
run_test "$script" '
export SKIP_END4_SETUP=1
bash fish.sh --skip-watermark
[ -f /root/.config/fish/config.fish ] || { echo "fish config not installed"; exit 1; }
fish -c "type -q fisher" || { echo "fisher not installed in fish"; exit 1; }
# the fixed prerequisite check must have installed fish itself
command -v fish >/dev/null || { echo "fish missing"; exit 1; }
'
;;
LazyVim.sh)
run_test "$script" '
bash LazyVim.sh
[ -d /root/.config/nvim ] || { echo "nvim config not cloned"; exit 1; }
command -v node >/dev/null || { echo "no node after install"; exit 1; }
# timestamped backup only if a config existed; none here, so just assert
# the clone exists and nothing is named plain nvim.bak
[ ! -e /root/.config/nvim.bak ] || { echo "legacy .bak name used"; exit 1; }
'
;;
*)
echo "error: unknown smoke test '$script' (known: bat.sh zsh.sh fish.sh LazyVim.sh)" >&2
exit 1
;;
esac
done
echo ""
echo "=== smoke summary ==="
printf '%s\n' "${RESULTS[@]}"
exit "$overall"
+30 -21
View File
@@ -25,8 +25,8 @@ echo '
|___/
--------------- ZSH Install Script ---------------
BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE UNDER THE GPL-2.0 LICENCE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. See the LICENCE for more detail
BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE UNDER THE GPL-2.0 LICENSE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. See the LICENSE for more detail
'
fi
@@ -40,7 +40,7 @@ set -eu
if ! command -v zsh >/dev/null || ! command -v git >/dev/null || ! command -v curl >/dev/null || ! command -v fzf >/dev/null; then
if command -v pacman >/dev/null; then
echo "pacman detected. Installing prerequisites"
sudo pacman -S zsh git curl fzf --noconfirm
sudo pacman -S --needed --noconfirm zsh git curl fzf
fi
if command -v apt >/dev/null; then
@@ -67,25 +67,32 @@ echo '
# Print commands
set -x
# Disable exit on error
set +eu
rm -rf ~/.oh-my-zsh
# Enable exit on error
set -eu
# Remove an existing oh-my-zsh install, but only when it really is one
if [ -d "$HOME/.oh-my-zsh" ]; then
if [ -f "$HOME/.oh-my-zsh/oh-my-zsh.sh" ]; then
echo "Removing existing ~/.oh-my-zsh for a clean install"
rm -rf "$HOME/.oh-my-zsh"
else
echo "warning: ~/.oh-my-zsh exists but does not look like an oh-my-zsh install - leaving it in place" >&2
fi
fi
# Do not print commands
set +x
# Backup the user's current config before oh-my-zsh overwrites it
if [ -f "$HOME/.zshrc" ]; then
if ! cp "$HOME/.zshrc" "$HOME/.zshrc.bak"; then
echo "error: failed to back up $HOME/.zshrc to $HOME/.zshrc.bak" >&2
# Backup the user's current configs before oh-my-zsh / our downloads overwrite them.
# Timestamped so re-running the script never overwrites the previous backup.
BACKUP_STAMP="$(date +%Y%m%d-%H%M%S)"
for cfg in .zshrc .zshenv; do
if [ -f "$HOME/$cfg" ]; then
cfg_backup="$HOME/$cfg.bak-$BACKUP_STAMP"
if ! cp "$HOME/$cfg" "$cfg_backup"; then
echo "error: failed to back up $HOME/$cfg to $cfg_backup" >&2
exit 1
fi
echo "Backed up $HOME/.zshrc to $HOME/.zshrc.bak"
fi
echo "Backed up $HOME/$cfg to $cfg_backup"
fi
done
# Install oh-my-zsh without entering zsh
if ! omz_installer=$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh); then
@@ -117,14 +124,16 @@ clone_plugin "$ZSH_CUSTOM_DIR/plugins/zsh-autocomplete" --depth=1 https://github
# Download and replace config file
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/configs/.zshrc -o ~/.zshrc
CONFIG_BASE_URL="https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/configs"
curl -fsSL "$CONFIG_BASE_URL/.zshrc" -o ~/.zshrc
curl -fsSL "$CONFIG_BASE_URL/.zshenv" -o ~/.zshenv
# Download generic fzf configuration
curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/configs/.fzf.zsh -o ~/.fzf.zsh
curl -fsSL "$CONFIG_BASE_URL/.fzf.zsh" -o ~/.fzf.zsh
# Setup fzf
mkdir -p ~/.fzf/shell
touch ~/.fzf/shell/key-bindings.zsh
# Setup fzf: the oh-my-zsh fzf-zsh-plugin provides the integration.
# (Previously an empty ~/.fzf/shell/key-bindings.zsh placeholder was created
# here - nothing ever sourced it, and it would shadow real bindings.)
# Do not print commands
set +x