From 10caebad29e48bf2ecdfe983a8de36fdcd9bf796 Mon Sep 17 00:00:00 2001 From: Jeremy McClure Date: Wed, 29 Jul 2026 11:30:49 -0400 Subject: [PATCH] zsh: add zoxide/bat/delta, starship prompt config, extra history options; rewrite README Bootstrap (zshrc-bootstrap.zsh): - Add bat and zoxide to the cross-distro install loop - Add git-delta install block (package is 'git-delta' on every PM but binary is 'delta', mirroring the starship/lazydocker pattern) - Debian/Ubuntu compat: symlink batcat->bat and fdfind->fd into ~/.local/bin so zshrc integrations work under both names - Bump ZSHRC_BOOTSTRAP_VERSION 2 -> 3 so existing machines re-run the bootstrap and auto-install the new tools zshrc: - Add setopts HIST_VERIFY, EXTENDED_HISTORY, AUTO_PUSHD, PUSHD_IGNORE_DUPS, PUSHD_SILENT - Self-configuring tool integrations (all behind command -v guards): - zoxide: eval $(zoxide init zsh) for frecent 'z'/'zi' dir jumping - bat: alias cat->bat, BAT_THEME, colored MANPAGER via bat - delta: GIT_PAGER=delta for prettier git diff/log/show - Extend the interactive auto-update block to also sync starship.toml to ~/.config/starship.toml (overwrite-if-changed, validated the same way as zshrc); keep the reproducible/self-configuring design - Inverted hyperlink fix already landed in prior commit; no change here starship.toml (new): - Two-line prompt: directory + git status + cmd duration on line 1, arrow (red on error) on line 2; Nerd Font symbols, SSH host/user, Python/Node/Rust/Go/Java/Docker context blocks - Validated clean with starship 1.21.1 (uses [nodejs], not [node]) README.md: - Full rewrite: documents the design philosophy, all files, deps, integrations, history options, aliases, keybindings, the eza hyperlink workaround rationale, auto-update behavior, per-machine overrides, env vars, and helper functions --- zsh/README.md | 127 ++++++++++++++++++++++++++++++++++++++-- zsh/starship.toml | 101 ++++++++++++++++++++++++++++++++ zsh/zshrc | 49 ++++++++++++++-- zsh/zshrc-bootstrap.zsh | 28 ++++++++- 4 files changed, 294 insertions(+), 11 deletions(-) create mode 100644 zsh/starship.toml diff --git a/zsh/README.md b/zsh/README.md index 83753c1..ca55dc4 100644 --- a/zsh/README.md +++ b/zsh/README.md @@ -1,12 +1,127 @@ # Zsh Config +A reproducible, self-configuring Zsh setup managed from a remote git repo. The `zshrc` auto-updates itself and the Starship prompt config on every interactive shell start, and a versioned bootstrap script installs all missing dependencies across distros. Drop `~/.zshrc` in place once and every machine stays in sync automatically. + +## Design + +- **Reproducible** — one `zshrc` drives an identical environment across every machine and distro. +- **Self-configuring** — tool integrations are guarded by `command -v`, so the shell works degraded-but-fine when a tool is absent, and lights up automatically once the bootstrap installs it. +- **Self-updating** — on each interactive shell start, `zshrc` and `starship.toml` are re-pulled from the repo and applied if changed (downloaded files are validated with `zsh -n` before replacing anything, so a 502 error page or corrupted content can't break the shell). +- **Network-resilient** — all remote calls use `--fail` + `--connect-timeout`/`--max-time`, so an unreachable server silently no-ops instead of hanging the shell. + ## Files -### `zshrc` -Main Zsh configuration file. Sets up Oh My Zsh, plugins (git, sudo, eza, fzf, starship, etc.), history options, aliases, and auto-updates itself from a remote source. Before replacing `~/.zshrc`, the downloaded file is validated with `zsh -n` to ensure it's syntactically valid — this prevents error pages or corrupted content from breaking the shell. Also bootstraps tmux config and runs a fastfetch system info display on interactive shells. +| File | Description | +|------|-------------| +| [`zshrc`](zshrc) | Main config. Path setup, history options, oh-my-zsh + plugins, eza theme, tool integrations, aliases, and the auto-update logic. Sources `~/.zshrc.local` at the end for per-machine overrides. | +| [`zshrc-bootstrap.zsh`](zshrc-bootstrap.zsh) | One-shot setup script run by `zshrc` when the bootstrap version changes. Installs all dependencies across distros and clones oh-my-zsh + custom plugins. | +| [`starship.toml`](starship.toml) | Starship prompt config — two-line prompt with directory, git status, command duration, Nerd Font symbols, and an error indicator. Synced to `~/.config/starship.toml` by the auto-update logic. | +| [`tmux.conf`](tmux.conf) | Tmux config with GitHub Dark theming, Vim-style pane navigation (`h/j/k/l`), TPM plugin manager, Nerd Font auto-install, and a built-in cheatsheet (`prefix + ?`). Installed to `~/.config/tmux/tmux.conf` on first shell start. | -### `zshrc-bootstrap.zsh` -One-shot setup script executed by `zshrc`. Installs missing dependencies (starship, lazydocker, fastfetch, eza, fzf, etc.) via the system package manager (brew, apt, dnf, pacman) or cargo, clones Oh My Zsh and custom plugins (zsh-autosuggestions, fast-syntax-highlighting, fzf-tab). Runs only when the bootstrap version changes. +## Getting Started -### `tmux.conf` -Tmux configuration with GitHub Dark theming, Nerd Font auto-install, TPM plugin manager, pane/window keybindings (Vim-style navigation with `h/j/k/l`), status bar showing session info, battery, online status, and a built-in cheatsheet (bound to `prefix + ?`). +Point `~/.zshrc` at the remote `zshrc`: + +```bash +curl -fsSL https://git.jeremymcclure.com/jeremy/scripts/raw/branch/master/zsh/zshrc -o ~/.zshrc && exec zsh +``` + +On first start, `zshrc` bootstraps missing dependencies (oh-my-zsh, plugins, and all CLI tools below) over the next shell starts. To force a re-bootstrap (e.g., after adding new tool installs to the bootstrap script), the bootstrap version is bumped in `zshrc` and every machine re-runs it automatically — or run manually: + +```bash +BOOTSTRAP=true exec zsh +``` + +## Dependencies + +All installed automatically by the bootstrap across brew / apt / dnf / pacman / cargo: + +| Tool | Purpose | +|------|---------| +| starship | Prompt (configured via `starship.toml`) | +| fzf | Fuzzy finder — powers `fzf-tab` completion and `zi` | +| eza | `ls` replacement with icons, git status, hyperlinks | +| bat | `cat` replacement + colored man pages | +| zoxide | Frecency directory jumping (`z`, `zi`) | +| git-delta | Pretty `git diff`/`log`/`show` pager | +| fastfetch | System info on shell start | +| lazydocker | TUI for Docker | +| rsync / tmux / git | Core utilities | + +Debian/Ubuntu renames handled: `batcat`→`bat`, `fdfind`→`fd` (symlinked into `~/.local/bin`). + +## Oh-My-Zsh Plugins + +`git`, `sudo`, `extract`, `eza`, `history`, `kitty`, `docker`, `docker-compose`, `archlinux`, `encode64`, `universalarchive`, `zsh-autosuggestions`, `fast-syntax-highlighting`, `fzf`, `fzf-tab`, `systemd`, `vscode`, `rsync`, `starship`. + +## Tool Integrations + +Each is enabled only when the binary is present (`command -v`): + +- **zoxide** — `z ` jumps to frecency-ranked dirs; `zi` opens the fzf picker. `cd` still works. +- **bat** — `alias cat='bat'`, `BAT_THEME=Monokai Extended`, and `MANPAGER` set so `man` pages are colored and paginated by bat. +- **delta** — `GIT_PAGER=delta` so `git diff`/`log`/`show` render side-by-side with syntax highlighting. (Not aliased to `diff` — delta is a pager reading diff input on stdin, not a `diff a b` replacement.) + +## History & Shell Options + +`SHARE_HISTORY`, `HIST_IGNORE_DUPS`, `HIST_IGNORE_ALL_DUPS`, `HIST_REDUCE_BLANKS`, `HIST_FCNTL_LOCK`, `HIST_VERIFY`, `EXTENDED_HISTORY`, `AUTO_CD`, `AUTO_LIST`, `INTERACTIVE_COMMENTS`, `AUTO_PUSHD`, `PUSHD_IGNORE_DUPS`, `PUSHD_SILENT`. + +`AUTO_PUSHD` maintains an automatic directory stack, so `cd -` and `dirs` let you navigate recent locations without `pushd`/`popd` muscle memory. + +## Aliases + +| Alias | Action | +|-------|--------| +| `c` | `clear` | +| `q` | `exit` | +| `open-ports` | `ss -tulpn \| grep LISTEN` | +| `nbstat` | Netbird peer status as a TSV table | +| `yeet` | `yay -Rcs` (remove + deps + config on Arch) | + +## Keybindings + +| Shortcut | Action | +|----------|--------| +| `Ctrl+R` | fzf history search (via `fzf` plugin) | +| `Ctrl+T` | fzf file finder | +| `Alt+C` | fzf cd | +| `Tab` | `fzf-tab` completion menu | +| `Esc` twice | Toggle `sudo` prefix (via `sudo` plugin) | + +## Eza Hyperlinks — Implementation Note + +`zshrc` does **not** set the oh-my-zsh `eza` plugin's `hyperlink` zstyle. The plugin emits a bare `--hyperlink` flag, which eza (clap) treats as taking the *next token* as its optional value — so `la /` fails with `invalid value '/'`. Instead, after oh-my-zsh loads, the aliases are rewritten to append the glued `--hyperlink=always` form, which can't eat a following path argument. Hyperlinks stay on and paths keep working. + +## Auto-Update Behavior + +On every interactive shell start: + +1. **zshrc** — re-downloaded, validated with `zsh -n`, and applied via `exec zsh` only if it differs from `~/.zshrc`. +2. **starship.toml** — re-downloaded and moved to `~/.config/starship.toml` if changed (applied live on the next prompt render; no shell reload needed). +3. **tmux.conf** — downloaded once on first shell start if `~/.config/tmux/tmux.conf` and `~/.config/byobu/.tmux.conf` are both absent. +4. **bootstrap** — runs (`zshrc-bootstrap.zsh`) when `ZSHRC_BOOTSTRAP_VERSION` in `zshrc` doesn't match `~/.config/zsh/.bootstrapped`. + +All downloads use `--fail` so a 502/HTML error page is never written over a working config, and `--connect-timeout`/`--max-time` so an unreachable server never hangs startup. + +## Per-Machine Overrides + +`~/.zshrc.local` is sourced at the very end of `zshrc` (created empty if missing). Put machine-specific aliases, env vars, or PATH additions there — they survive repo updates since `zshrc` doesn't manage that file. + +## Environment Variables + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ZSHRC_GIT` | `.../scripts/raw/branch/master/zsh/` | Base URL for all remote config files | +| `ZSHRC_BOOTSTRAP_VERSION` | `3` | Bump to force a re-bootstrap on all machines | +| `FFENABLED` | (unset → enabled) | Set to `false` to disable the fastfetch system info on shell start | +| `BAT_THEME` | `Monokai Extended` | Override before the bat block to use a different bat theme | +| `GIT_PAGER` | `delta` (when installed) | Override to use a different git pager | + +## Helper Functions + +```bash +update_zshrc # Manually re-pull and reload the zshrc +fix_btopbg # Disable btop theme_background (workaround for transparency) +install_opencode # Install the opencode CLI +BOOTSTRAP=true exec zsh # Force a re-bootstrap +``` \ No newline at end of file diff --git a/zsh/starship.toml b/zsh/starship.toml new file mode 100644 index 0000000..6a1d1f6 --- /dev/null +++ b/zsh/starship.toml @@ -0,0 +1,101 @@ +# Starship prompt configuration - managed by the zshrc repo. +# Local edits to ~/.config/starship.toml are overwritten on the next interactive +# shell start (reproducible/self-configuring design). Uses Nerd Font symbols +# (JetBrainsMono Nerd Font is installed by the tmux config). + +command_timeout = 1000 +add_newline = true + +# Two-line prompt: +# line 1: directory + git status + command duration +# line 2: the input arrow (red on error) +format = """ +$directory\ +$git_branch\ +$git_status\ +$cmd_duration\ +$line_break\ +$character\ +""" + +[character] +success_symbol = "[➜](bold green)" +error_symbol = "[✗](bold red)" +vimcmd_symbol = "[V](bold green)" + +[directory] +truncation_length = 3 +truncate_to_repo = true +style = "bold blue" +read_only = " 󰌾" +read_only_style = "red" +repo_root_style = "bold cyan" + +[git_branch] +symbol = " " +style = "bold purple" +format = "[$symbol$branch]($style) " + +[git_status] +style = "bold red" +conflicted = "=${count}" +ahead = "⇡${count}" +behind = "⇣${count}" +diverged = "⇕↑${ahead_count}↓${behind_count}" +untracked = "?${count}" +stashed = " *${count}" +modified = " !${count}" +staged = " +${count}" +renamed = " »${count}" +deleted = " ✘${count}" +format = '([$all_status$ahead_behind]($style) )' + +[cmd_duration] +min_time = 2000 +format = "took [$duration](bold yellow) " + +[status] +disabled = false +format = '[$symbol]($style)' +symbol = "✗ " +success_symbol = "" +style = "bold red" + +[username] +show_always = false +style_user = "bold yellow" +style_root = "bold red" +format = "[$user]($style)@" + +[hostname] +ssh_only = true +style = "bold green" +format = "[$hostname]($style) " + +[python] +symbol = " " +format = '[${symbol}${pyenv_prefix}(${version})(\($virtualenv\))]($style) ' + +[nodejs] +symbol = " " +format = "[$symbol($version)]($style) " + +[rust] +symbol = " " +format = "[$symbol($version)]($style) " + +[golang] +symbol = " " +format = "[$symbol($version)]($style) " + +[java] +symbol = " " +format = "[$symbol($version)]($style) " + +[docker_context] +symbol = " " +format = "[$symbol$context]($style) " + +[shell] +disabled = true +style = "bold cyan" \ No newline at end of file diff --git a/zsh/zshrc b/zsh/zshrc index fa0f223..5e106a6 100644 --- a/zsh/zshrc +++ b/zsh/zshrc @@ -6,7 +6,7 @@ export ZSHRC_GIT="https://git.jeremymcclure.com/jeremy/scripts/raw/branch/master export ZSHRC_URL="$ZSHRC_GIT/zshrc" export ZSHRC_BOOTSTRAP_URL="$ZSHRC_GIT/zshrc-bootstrap.zsh" -export ZSHRC_BOOTSTRAP_VERSION="2" +export ZSHRC_BOOTSTRAP_VERSION="3" export ZSHRC_BOOTSTRAP="$ZSH_CONFIG/.bootstrapped" [[ -d "$HOME/Scripts" ]] && path+=("$HOME/Scripts") @@ -76,6 +76,11 @@ setopt HIST_FCNTL_LOCK setopt AUTO_CD setopt AUTO_LIST setopt INTERACTIVE_COMMENTS +setopt HIST_VERIFY +setopt EXTENDED_HISTORY +setopt AUTO_PUSHD +setopt PUSHD_IGNORE_DUPS +setopt PUSHD_SILENT ZSH_THEME="" ENABLE_CORRECTION="false" @@ -132,21 +137,57 @@ for _ea in la ldot lD lDD ll ls lsd lsdl lS lT; do done unset _ea +# --- Tool integrations (self-configuring: enabled only when the tool exists) --- + +# zoxide: frecency-based directory jumping — `z ` / `zi` (fzf picker). +# Replaces nothing; `cd` still works. Auto-installed by the bootstrap. +command -v zoxide >/dev/null && eval "$(zoxide init zsh)" 2>/dev/null + +# bat: colored `cat` and colored man pages. bat/batcat handled by bootstrap. +if command -v bat >/dev/null; then + alias cat='bat' + export BAT_THEME="${BAT_THEME:-Monokai Extended}" + export MANPAGER="sh -c 'col -bx | bat -l man -p'" + export MANROFFOPT='-c' +fi + +# delta: prettier git diffs as git's pager (git respects GIT_PAGER). Auto-installed +# by the bootstrap. Not aliased to `diff` since delta reads diff input on stdin, +# not two file arguments — GIT_PAGER is the correct integration. +command -v delta >/dev/null && export GIT_PAGER='delta' + alias c='clear' alias q="exit" alias nbstat=$'netbird status --json | jq -r \'.peers.details[]? | [(.hostname // .fqdn), .netbirdIp, .status] | @tsv\' | column -t -s $\'\\t\'' alias open-ports="ss -tulpn | grep LISTEN" alias yeet='yay -Rcs' -# Auto-update: check every shell start, silently fail if server unreachable +# Auto-update self and managed configs: checked every interactive shell start, +# silently skipped if the server is unreachable. Keeps zshrc + starship.toml in +# sync across machines (reproducible/self-configuring); only reloads the shell +# when the zshrc itself changes (starship applies its config live per render). if [[ -o interactive ]]; then + # zshrc (reload shell if changed) tmp=/tmp/.zshrc.$$ - curl -fsSL --connect-timeout 3 --max-time 8 "$ZSHRC_URL" -o "$tmp" 2>/dev/null - if [[ -f "$tmp" ]] && zsh -n "$tmp" 2>/dev/null && ! cmp -s "$tmp" "$HOME/.zshrc" 2>/dev/null; then + if curl -fsSL --connect-timeout 3 --max-time 8 "$ZSHRC_URL" -o "$tmp" 2>/dev/null \ + && [[ -f "$tmp" ]] && zsh -n "$tmp" 2>/dev/null \ + && ! cmp -s "$tmp" "$HOME/.zshrc" 2>/dev/null; then echo "Updating zshrc..." mv "$tmp" "$HOME/.zshrc" && exec zsh fi rm -f "$tmp" + + # starship prompt config (overwrite if changed; applied live on next render) + if command -v starship >/dev/null; then + stmp=$(mktemp) + if curl -fsSL --connect-timeout 3 --max-time 8 "$ZSHRC_GIT/starship.toml" -o "$stmp" 2>/dev/null \ + && [[ -f "$stmp" ]] && ! cmp -s "$stmp" "$HOME/.config/starship.toml" 2>/dev/null; then + mkdir -p "$HOME/.config" + mv "$stmp" "$HOME/.config/starship.toml" + else + rm -f "$stmp" + fi + fi fi [[ -f $HOME/.zshrc.local ]] || touch $HOME/.zshrc.local diff --git a/zsh/zshrc-bootstrap.zsh b/zsh/zshrc-bootstrap.zsh index 1ac7f3c..c899f08 100644 --- a/zsh/zshrc-bootstrap.zsh +++ b/zsh/zshrc-bootstrap.zsh @@ -7,7 +7,7 @@ mkdir -p "$HOME/.local/bin" mkdir -p "$ZSH_CONFIG" path+=("$HOME/.local/bin") -for p in git starship fzf eza rsync lazydocker fastfetch tmux; do +for p in git starship fzf eza rsync lazydocker fastfetch tmux bat zoxide; do command -v $p >/dev/null && continue if command -v brew >/dev/null; then echo "Installing $p using brew" && brew install $p && continue @@ -37,6 +37,32 @@ if ! command -v lazydocker >/dev/null; then DIR=$HOME/.local/bin bash >/dev/null 2>&1 fi +# Debian/Ubuntu ships bat as 'batcat' and fd as 'fdfind'; symlink to the +# canonical names so the zshrc integrations (which call bat/fd) work everywhere. +if ! command -v bat >/dev/null && command -v batcat >/dev/null; then + ln -sf "$(command -v batcat)" "$HOME/.local/bin/bat" +fi +if ! command -v fd >/dev/null && command -v fdfind >/dev/null; then + ln -sf "$(command -v fdfind)" "$HOME/.local/bin/fd" +fi + +# git-delta: the package is named 'git-delta' on every package manager but the +# binary is 'delta' (name mismatch vs the install loop above), so install via the +# same PM cascade using the real package name. Used by zshrc as GIT_PAGER. +if ! command -v delta >/dev/null; then + if command -v brew >/dev/null; then + echo "Installing git-delta using brew" && brew install git-delta + elif command -v apt >/dev/null && apt-cache show git-delta >/dev/null 2>&1; then + echo "Installing git-delta using Apt" && sudo apt install git-delta -y + elif command -v dnf >/dev/null && dnf list available git-delta >/dev/null 2>&1; then + echo "Installing git-delta using dnf" && sudo dnf install git-delta -y + elif command -v pacman >/dev/null && pacman -Si git-delta >/dev/null 2>&1; then + echo "Installing git-delta using pacman" && sudo pacman -Sy --noconfirm git-delta + elif command -v cargo >/dev/null; then + echo "Installing git-delta using cargo" && cargo install git-delta --locked + fi +fi + arch=$(uname -m) case "$arch" in x86_64) target_fast="linux-amd64" && target_eza="x86_64-unknown-linux-gnu" ;;