Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ fallback and the unapplied-pins path - 28 cells total.
Also avoid `[ cond ] && var=true` - when the condition is false, the expression exits with 1 and
trips `set -e`. Use `if [ cond ]; then var=true; fi` instead.
- Every function variable must be declared `local` (or `local -a` for arrays).
- A tool whose Debian package renames the binary needs a SHIM, or the installer
skips it forever: `has bat` is false when apt supplied `batcat`, exactly as with
`fd`/`fdfind`. Add the shim in the same change as the installer, in BOTH
`install_base` and `install_base_docker` - and note the docker branch needs it
even though its apt list does not ask for the package, since a base image may
already carry the renamed binary.
- **Run `bash tests/lint-workflows.sh` before pushing a workflow change.** An
invalid workflow runs nothing, so CI cannot report the fault, and with
`CI gate` required the PR blocks with no visible cause. It catches the two
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.13.0] - 2026-09-19

### Added
- `bat` is now installed and updated like the other tools: apt where available, GitHub binary otherwise, with a `bat` shim when apt supplies `batcat`. `test.sh` checks it, and the no-sudo profile checks it strictly in `~/.local/bin`.
- `test.sh` now also checks tmux strictly in `~/.local/bin` for the no-sudo profile, covering the installer added in 1.12.0.

## [1.12.0] - 2026-09-19

### Added
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.12.0
1.13.0
52 changes: 51 additions & 1 deletion modules/base.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ install_base() {
jq
man-db gnupg
python3 python3-venv
ripgrep fd-find tig
ripgrep fd-find tig bat
parallel shellcheck
)
log_info "Installing via apt: ${_pkgs[*]} (versions resolved by apt)"
Expand Down Expand Up @@ -48,12 +48,19 @@ install_base() {
log_ok "Created fd → fdfind shim in ~/.local/bin"
fi

# Same story for bat: Debian/Ubuntu install the binary as 'batcat'.
if ! has bat && has batcat; then
ln -sf "$(command -v batcat)" ~/.local/bin/bat
log_ok "Created bat → batcat shim in ~/.local/bin"
fi

# Cover what apt could not supply (no ripgrep/fd-find before 19.10).
# Each installer returns early when the binary is already present, so
# this is a no-op on releases where apt carried them.
_install_ripgrep
_install_fd
_install_jq
_install_bat
else
if $CAN_SUDO; then
log_warn "No apt on this system - skipping system packages; fetching tools as local binaries"
Expand All @@ -70,6 +77,7 @@ install_base() {
_install_ripgrep
_install_fd
_install_jq
_install_bat
_install_tmux
fi

Expand Down Expand Up @@ -115,11 +123,18 @@ install_base_docker() {
if ! has fd && has fdfind; then
ln -sf "$(command -v fdfind)" ~/.local/bin/fd
fi
# Needed even though this list does not ask apt for bat: a base image may
# already carry batcat, and _install_bat treats that as installed - without
# the shim there would then be no `bat` on PATH at all.
if ! has bat && has batcat; then
ln -sf "$(command -v batcat)" ~/.local/bin/bat
fi

# Cover what apt could not supply; each installer self-skips when present.
_install_ripgrep
_install_fd
_install_jq
_install_bat
else
log_warn "No apt - skipping system packages; fetching tools as local binaries"
# git, zsh and python3 have no practical single-binary fallback, so they
Expand All @@ -132,6 +147,7 @@ install_base_docker() {
_install_ripgrep
_install_fd
_install_jq
_install_bat
_install_tmux
fi

Expand Down Expand Up @@ -461,6 +477,40 @@ _install_ripgrep() {
fi
}

# bat: GitHub tarball - binary is 'bat'.
# Debian/Ubuntu ship the binary as 'batcat' (the name 'bat' was already taken),
# exactly like fd/fdfind, so both names count as installed and the shim in
# install_base puts a 'bat' on PATH when apt supplied batcat.
_install_bat() {
if has bat || has batcat; then
log_ok "bat already installed - skipping"
return
fi
log_step "bat (GitHub binary)"
local arch; arch=$(uname -m)
local bat_arch
case "$arch" in
x86_64) bat_arch="x86_64-unknown-linux-musl" ;;
aarch64) bat_arch="aarch64-unknown-linux-gnu" ;;
*)
log_warn "bat: unsupported arch $arch - skipping"
return
;;
esac
local tag="" url=""
read -r tag url < <(_gh_release_info "sharkdp/bat" "${bat_arch}.tar.gz") || true
if [ -z "$url" ]; then
log_warn "bat: could not find release URL - skipping"
return
fi
log_info "bat: installing ${tag:-unknown} → ~/.local/bin/bat"
if _download_tar_bin "$url" "bat" ~/.local/bin/bat; then
log_ok "bat installed → ~/.local/bin/bat ($(~/.local/bin/bat --version 2>/dev/null))"
else
log_warn "bat: download failed - skipping"
fi
}

# tmux: only reachable here as a static AppImage.
#
# This repo ships .tmux.conf, .tmux.conf.local and clones tmux plugins, so a host
Expand Down
26 changes: 21 additions & 5 deletions test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ _hdr() { echo -e "\n${BOLD}── $* ──${NC}"; }

# ── Helpers ───────────────────────────────────────────────────────────────────
check_cmd() {
local cmd="$1" label="${2:-$1}"
# $3 is the version flag, for tools that do not answer --version: tmux prints
# its usage instead, which was being reported as the version string.
local cmd="$1" label="${2:-$1}" vflag="${3:---version}"
if command -v "$cmd" &>/dev/null; then
_ok "$label → $(command -v "$cmd") ($(${cmd} --version 2>&1 | head -1))"
_ok "$label → $(command -v "$cmd") ($("$cmd" "$vflag" 2>&1 | head -1))"
else
_fail "$label not found"
fi
Expand Down Expand Up @@ -109,7 +111,7 @@ check_link ~/.gitattributes
# ── 3. Core tools ──────────────────────────────────────────────────────────────
_hdr "Core tools"
check_cmd zsh
check_cmd tmux
check_cmd tmux "tmux" -V
check_cmd git
check_cmd python3
check_cmd fzf
Expand All @@ -127,6 +129,15 @@ else
_fail "fd / fdfind not found"
fi

# bat has the same Debian/Ubuntu rename as fd: the binary ships as 'batcat'.
if command -v bat &>/dev/null; then
_ok "bat → $(command -v bat) ($(bat --version 2>&1 | head -1))"
elif command -v batcat &>/dev/null; then
_ok "bat (as batcat) → $(command -v batcat)"
else
_fail "bat / batcat not found"
fi

# ── 4. fzf shell integration ───────────────────────────────────────────────────
_hdr "fzf shell integration"
# fzf is installed via git clone to ~/.fzf; the installer generates ~/.fzf.zsh
Expand Down Expand Up @@ -393,10 +404,12 @@ if [ "$PROFILE" = "nosudo" ]; then
_hdr "No-sudo: ~/.local/bin binaries"

check_local_bin() {
local cmd="$1" label="${2:-$1}"
# $3 is the version flag: tmux answers -V and prints its usage for
# --version, which would otherwise be reported as the "version".
local cmd="$1" label="${2:-$1}" vflag="${3:---version}"
local p="$HOME/.local/bin/$cmd"
if [ -x "$p" ]; then
_ok "$label → $p ($("$p" --version 2>&1 | head -1))"
_ok "$label → $p ($("$p" "$vflag" 2>&1 | head -1))"
else
# Strict: must be in ~/.local/bin - not just anywhere on PATH.
# For nosudo-forced this verifies NOSUDO=1 was respected (sudo was
Expand All @@ -407,12 +420,15 @@ if [ "$PROFILE" = "nosudo" ]; then

check_local_bin rg "ripgrep"
check_local_bin fd "fd"
check_local_bin bat "bat"
check_local_bin jq "jq"
check_local_bin fzf "fzf"
check_local_bin zoxide "zoxide"
check_local_bin delta "git-delta"
check_local_bin eza "eza"
check_local_bin yazi "yazi"
# tmux is a no-sudo install too since 1.12.0 (static AppImage).
check_local_bin tmux "tmux" -V

_hdr "No-sudo: sudo availability"
# nosudo-auto: sudo binary absent -> detect_sudo() auto-detected CAN_SUDO=false
Expand Down
20 changes: 18 additions & 2 deletions update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# --help, -h Show this message
#
# Available tools (pass one or more to update only those):
# apt omz tmux-plugins zsh-plugins fzf rg fd shellcheck
# apt omz tmux-plugins zsh-plugins fzf rg fd bat shellcheck
# zoxide delta eza yazi uv ruff neovim tree-sitter cheat pre-commit xcape
#
# A PATH shadow check always runs at the end (read-only). It detects older
Expand Down Expand Up @@ -49,7 +49,7 @@ while [[ $# -gt 0 ]]; do
shift
done

_KNOWN_TOOLS=(apt omz tmux-plugins zsh-plugins fzf rg fd shellcheck
_KNOWN_TOOLS=(apt omz tmux-plugins zsh-plugins fzf rg fd bat shellcheck
zoxide delta eza yazi uv ruff neovim tree-sitter cheat pre-commit xcape)

# Validate SELECTED against known tool names.
Expand Down Expand Up @@ -494,6 +494,22 @@ if _should_run fd; then
fi
fi

# ── bat ────────────────────────────────────────────────────────────────────────
# Same special case as fd: Debian/Ubuntu install the binary as 'batcat', so both
# names count as present and the updated binary always lands at ~/.local/bin/bat.
if _should_run bat; then
log_step "bat"
if has bat || has batcat; then
case "$ARCH" in
x86_64) _gh_update_binary bat "sharkdp/bat" "x86_64-unknown-linux-musl.tar.gz" bat ~/.local/bin/bat || true ;;
aarch64) _gh_update_binary bat "sharkdp/bat" "aarch64-unknown-linux-gnu.tar.gz" bat ~/.local/bin/bat || true ;;
*) log_warn "bat: unsupported arch $ARCH - skipping" ;;
esac
else
log_warn "bat not installed - skipping"
fi
fi

# ── shellcheck ─────────────────────────────────────────────────────────────────
# Uses bare arch names (x86_64/aarch64), not Rust triples - keep its own section.
if _should_run shellcheck; then
Expand Down
Loading