Modern-CLI aliases (eza, bat, rg, fd…), each guarded by a capability check (HAVE_* flags) so a missing tool never breaks the shell.
# core/zsh/20-aliases.zsh
# ──────────────────────────────────────────────────────────────────────────────
# Aliases for the modern CLI stack. Every alias touching an optional tool is
# GUARDED by a HAVE_* flag from 00-tools.zsh, so on a bare box (fresh server, rescue
# shell) you transparently get the classic command. Load AFTER 00-tools.zsh.
# Anything offensive/engagement-flavoured lives in dotfiles-Kali, not here.
# ──────────────────────────────────────────────────────────────────────────────
# ── ls -> eza ─────────────────────────────────────────────────────────────────
if [[ -n ${HAVE_EZA:-} ]]; then
alias ls='eza --group-directories-first --icons=auto'
alias ll='eza -lah --group-directories-first --icons=auto --git'
alias la='eza -a --group-directories-first --icons=auto'
alias lt='eza --tree --level=2 --icons=auto'
alias llt='eza --tree --level=3 -l --icons=auto'
alias tree='eza --tree --icons=auto'
(($+functions[compdef])) && compdef eza=ls # reuse ls completion for eza
else
alias ll='ls -lah'
alias la='ls -A'
fi
# ── cat -> bat (resolved name from 00-tools.zsh) ────────────────────────────────
if [[ -n ${HAVE_BAT:-} ]]; then
alias cat="$BAT_BIN --paging=never"
alias catp="$BAT_BIN" # paged, full bat
# …and `bat` under its CANONICAL name, mirroring the fd line below. Debian/Ubuntu/Kali
# ship the binary as `batcat`, so without this the tool was installed and fully wired
# (cat, catp, MANPAGER, the fzf previews) yet untypeable by the name its own README, man
# page and every upstream recipe use. The two renamed tools were handled asymmetrically —
# fd got this alias, bat did not — which is also what made core-doctor report `✗ bat` two
# lines above a `resolved` section naming batcat. Harmless `alias bat=bat` elsewhere: zsh
# does not re-expand an alias to its own name in command position.
alias bat="$BAT_BIN"
export BAT_THEME="ansi" # follow the terminal palette (tokyonight via ghostty)
export MANPAGER="sh -c 'col -bx | $BAT_BIN -l man -p'"
fi
# ── find -> fd ────────────────────────────────────────────────────────────────
[[ -n ${HAVE_FD:-} ]] && alias fd="$FD_BIN"
# ── grep stays POSIX for scripts; rg is its own command (smart-case default) ──
[[ -n ${HAVE_RG:-} ]] && alias rg='rg --smart-case'
# ── cd -> zoxide (z), interactive jump (zi), `-` to previous dir ─────────────
if [[ -n ${HAVE_ZOXIDE:-} ]]; then
alias cd='z'
alias cdi='zi'
fi
alias -- -='cd -'
# ── disk / process / monitor ──────────────────────────────────────────────────
[[ -n ${HAVE_DUST:-} ]] && alias du='dust'
[[ -n ${HAVE_PROCS:-} ]] && alias ps='procs'
[[ -n ${HAVE_BTOP:-} ]] && alias top='btop' && alias htop='btop'
[[ -n ${HAVE_VIDDY:-} ]] && alias watch='viddy'
# df → duf (modern, mountpoint-aware); classic `df -h` stays the bare-box fallback.
if [[ -n ${HAVE_DUF:-} ]]; then alias df='duf'; else alias df='df -h'; fi
# ── file manager ──────────────────────────────────────────────────────────────
[[ -n ${HAVE_YAZI:-} ]] && {
alias fm='yazi'
alias y='yazi'
}
# ── terminal web browser (w3m preferred; BROWSER_BIN resolved in 00-tools.zsh) ──
# When a browser is present, `web <url>` is defined; with none installed this whole
# block is skipped — no alias, no export (the "no browser" row of the PR's table).
# $BROWSER is claimed only on a headless box (SSH / server / WSL-no-X) so it never
# hijacks GUI-opening tools on a desktop; macOS ($OSTYPE=darwin*) always has a GUI,
# so it's skipped too.
if [[ -n ${HAVE_BROWSER:-} ]]; then
alias web="$BROWSER_BIN"
if [[ -z ${DISPLAY:-} && -z ${WAYLAND_DISPLAY:-} && $OSTYPE != darwin* ]]; then
export BROWSER="$BROWSER_BIN"
fi
fi
# ── 2026 modern stack additions (all guarded; classics untouched) ────────────
# xh: Rust HTTPie — for poking APIs / web targets. curl stays for scripts.
[[ -n ${HAVE_XH:-} ]] && {
alias http='xh'
alias https='xh --https'
}
# glow: render markdown in the terminal (engagement notes, READMEs)
[[ -n ${HAVE_GLOW:-} ]] && alias md='glow --pager'
# doggo: modern dig (DNS recon). dig stays as-is; this is a distinct verb.
[[ -n ${HAVE_DOGGO:-} ]] && alias dns='doggo'
# gron / sd are their own commands (no alias — never shadow sed in scripts).
# jq / yq / jnv / lnav / hyperfine / watchexec / shellcheck / shfmt are likewise their own
# commands: they shadow nothing classic, so they get HAVE_* detection in 00-tools.zsh but
# no alias.
# (jnv is the interactive JSON explorer — you run `jnv file.json` or pipe into it.)
# (lnav is the log reader — `lnav /var/log/...` or a directory; it merges and follows.)
# (watchexec re-runs a command on file changes — `watchexec -e py -- pytest`. NOT aliased
# to `watch`: 20-aliases.zsh already points `watch` at viddy, and conflating "re-run on a
# timer" with "re-run on a change" would silently give you the wrong one.)
# ── editor + misc QoL ─────────────────────────────────────────────────────────
alias vim='nvim'
# diff: colourise ONLY when this box's diff actually supports `--color` (GNU does;
# BSD/macOS diff — the dotfiles-MacBook target — and busybox diff on Alpine do NOT,
# where an unconditional alias would make every `diff` invocation error). `--color`
# support is a STABLE property of the box's diff binary, so probing it forks the real
# `diff` on every shell for an answer that never changes. Cache the verdict keyed on the
# binary's mtime (the same invalidation _cache_eval uses): re-probe only when diff is
# newer than the cache — e.g. after a GNU/BSD toolchain change. When the cache dir isn't
# writable the live probe still decides correctly, so correctness never depends on the
# cache. (df → duf/df -h above.)
() {
emulate -L zsh
local bin="${commands[diff]}" cache="${XDG_CACHE_HOME:-$HOME/.cache}/zsh/diff-color"
[[ -z "$bin" ]] && return # no diff at all → no alias
if [[ -e "$cache" && ! "$bin" -nt "$cache" ]]; then
[[ -s "$cache" ]] && alias diff='diff --color=auto' # fresh cache, zero forks
return
fi
# (re)probe once, then persist the verdict (non-empty = supported) for next start.
# `>|` forces the write past 10-options.zsh's NO_CLOBBER (loaded before 20-aliases.zsh).
if diff --color=auto /dev/null /dev/null >/dev/null 2>&1; then
alias diff='diff --color=auto'
mkdir -p "${cache:h}" 2>/dev/null && print -rn -- 1 >| "$cache" 2>/dev/null
else
mkdir -p "${cache:h}" 2>/dev/null && print -rn -- '' >| "$cache" 2>/dev/null
fi
}
# ── git ───────────────────────────────────────────────────────────────────────
# The git alias set is the single source of truth in 25-git.zsh (OMZ-style, loaded
# right after this file). Two exceptions live here because they are TOOL-DETECTION
# gated, not git-workflow aliases: the `lg` lazygit launcher and the HAVE_DIFFT-gated
# `gdft` below.
# git-absorb gets NO alias at all — it installs as the `git absorb` subcommand, so git
# already dispatches it and there is nothing to shadow. 00-tools.zsh detects it
# (HAVE_GIT_ABSORB) purely so core-doctor can report it; see git/gitconfig's `fix` alias.
alias lg='lazygit'
# difftastic (difft): AST/structural diff — an OPT-IN companion to delta, never the
# default pager. delta stays the daily syntax-highlighting diff; `gdft [<ref>]` reviews
# a change by *structure*, so formatting-only churn (rewraps, moved elements, trailingShowing the first 140 of 200 lines — read the full file ↗