From bec96e845659c342a45311a522bd856d5717fd8f Mon Sep 17 00:00:00 2001 From: MangoPig Date: Mon, 1 Jun 2026 15:39:04 +0100 Subject: [PATCH] Add README and shell polish --- README.md | 337 ++++++++++++++++++++++++++++++++++++++++++++ Zsh/.zsh_completion | 175 +++++++++++++++++++++++ Zsh/.zsh_prompt | 46 +++++- Zsh/.zshrc | 49 +++++-- 4 files changed, 588 insertions(+), 19 deletions(-) create mode 100644 Zsh/.zsh_completion diff --git a/README.md b/README.md index e69de29..c26ba75 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,337 @@ +# Dot-Zsh + +Personal cross-platform shell and workstation bootstrap for: + +- Arch Linux +- Ubuntu / Debian-family Linux +- Fedora +- macOS +- WSL + +The repo is built around a small bootstrap path and a fuller `just`-based workflow. + +--- + +## Layout + +```text +. +├── Bins/ # Pinned binary versions (nvim, eza, ...) +├── Commands/ # just modules +├── Scripts/ # install/setup scripts +├── Zsh/ # stowed shell files +├── Justfile # main just entrypoint +└── Makefile # lightweight bootstrap entrypoint +``` + +### Key folders + +#### `Zsh/` + +Stowed into `$HOME`. + +- `.zshrc` +- `.zshenv` +- `.zsh_aliases` +- `.zsh_prompt` +- `.zsh_completion` +- `.zsh_secrets` +- `.zsh_secrets.example` + +#### `Scripts/` + +Main install logic. + +- `base.sh` — system/base packages and common tools +- `setup.sh` — full setup flow +- `node.sh`, `go.sh`, `rust.sh`, `python.sh`, `r.sh`, `cpp.sh` +- `bin/install.sh` — install pinned repo-managed binaries +- `bin/update.sh` — update pinned binary versions + +#### `Commands/` + +Modular `just` commands. + +- `Commands/Setup/mod.just` +- `Commands/Lang/mod.just` +- `Commands/Bin/mod.just` + +#### `Bins/` + +`versions.json` is the source of truth for pinned binary versions and per-platform asset mappings. + +--- + +## Core workflow + +### Bootstrap only + +Use `make setup` when you want the lightweight bootstrap path: + +```bash +make setup +``` + +This currently does: + +1. run `Scripts/base.sh` +2. remove old `Zsh` stow links +3. stow `Zsh/` into `$HOME` + +It is intentionally small. + +### Full setup + +Use `just setup all` for the full machine setup: + +```bash +just setup all +``` + +This runs the full flow in `Scripts/setup.sh`: + +1. base system setup +2. node +3. go / gvm +4. rust +5. python / pyenv / miniforge +6. r +7. secret-file reconciliation +8. backup of conflicting unmanaged dotfiles +9. stow `Zsh/` + +### Other useful commands + +```bash +just setup base +just setup stow +just setup clean +just setup update + +just lang node +just lang go +just lang rust +just lang python +just lang r +just lang cpp + +just bin show +just bin install-all +just bin install nvim +just bin list nvim +just bin update nvim +``` + +If `fzf` is available, `just bin update ` can use interactive selection. + +--- + +## Secrets model + +This repo now treats shell secrets as repo-managed local dotfiles instead of tracked plaintext config. + +### Source of truth + +```text +Zsh/.zsh_secrets +``` + +That file is intended to be stowed to: + +```text +$HOME/.zsh_secrets +``` + +### Important notes + +- `.zsh_secrets` is ignored by git +- `.zsh_secrets.example` exists as a template/reference +- setup tries to reconcile an existing `$HOME/.zsh_secrets` safely +- if conflicting unmanaged dotfiles already exist, setup backs them up before stowing + +Backup location: + +```text +~/.dotzsh-pre-stow-backup// +``` + +--- + +## Binary management + +This repo no longer stores shipped binaries in git. + +Instead: + +- pinned versions live in `Bins/versions.json` +- installs go into `~/.local/bin` +- updates happen through `just bin update ` + +Current managed binaries include: + +- `nvim` +- `eza` + +Platform behavior: + +- Linux: install from pinned release assets +- macOS: install from pinned asset when supported, or use Homebrew formula fallback when configured + +--- + +## Shell behavior + +### Prompt + +Prompt logic lives in: + +```text +Zsh/.zsh_prompt +``` + +It adds: + +- git branch +- git dirty/clean indicator +- conda / direnv context +- repo-scoped project markers +- language version hints +- separator rule between prompts + +### Completion + +Completion logic lives in: + +```text +Zsh/.zsh_completion +``` + +It adds: + +- custom first-word command completion +- recently used commands shown first +- used commands visually marked with `*` +- wrapper completion bindings for lazy-loaded tools like `gvm`, `conda`, `go`, `node`, `npm`, and `npx` + +### Lazy loading + +Several language managers are intentionally lazy-loaded to keep shell startup lighter. + +Examples: + +- `nvm` +- `gvm` +- `pyenv` +- `conda` + +`openchamber` auto-start is restricted to WSL sessions. + +--- + +## Install size + +Measured practical installed footprint on Linux is roughly: + +- Ubuntu: about `1.96 GB` +- Arch: about `1.84 GB` +- Fedora: about `1.77 GB` + +So the setup is best thought of as: + +```text +~1.8 GB to ~2.0 GB total +``` + +### Largest components + +Measured Ubuntu component breakdown: + +- Rust: about `1.5 GB` +- Python: about `1.1 GB` +- Go: about `635 MB` +- Node: about `235 MB` +- `~/.local`: about `128 MB` +- R wrapper/user area under `~/.programming/r`: very small (`~20 KB` in that measurement) + +Important nuance: + +- `~/.programming/r` is intentionally small because it mainly holds wrappers and user-library location +- the actual R runtime may still come from the distro package manager or Rig, depending on platform +- so not all R-related disk usage appears under `~/.programming/r` + +### Measured paths + +Representative measured path sizes from Linux test runs: + +```text +Ubuntu + ~/.programming ~3.17 GB + ~/.local ~133 MB + /usr/local ~13 MB + +Arch + ~/.programming ~3.17 GB + ~/.local ~133 MB + +Fedora + ~/.programming ~3.17 GB + ~/.local ~133 MB +``` + +Those path totals are larger than the final container image delta because some space is shared/overlapping across package layers and installed tooling. + +--- + +## Notes by platform + +### Linux + +- full setup has been exercised in container tests on Ubuntu, Arch, and Fedora x86_64 + +### macOS + +- supports Homebrew-based package install flow +- uses `/bin/zsh` for shell-change target +- handles existing dotfiles and secret symlink reconciliation more carefully + +### WSL + +- WSL-specific logic is used where needed +- `openchamber` is only auto-started in WSL + +--- + +## First-run recommendation + +If you are starting clean: + +```bash +make setup +just setup all +``` + +If you only want shell files re-linked: + +```bash +just setup clean +just setup stow +``` + +If you only want to refresh pinned binaries: + +```bash +just bin install-all +``` + +--- + +## Current philosophy + +This repo aims to be: + +- owned rather than magical +- reproducible rather than ad hoc +- modular rather than one giant dotfile blob +- explicit about pinned binaries and local secrets + +It is not trying to be the smallest possible install. It is trying to be a repeatable personal workstation setup. diff --git a/Zsh/.zsh_completion b/Zsh/.zsh_completion new file mode 100644 index 0000000..6b714f0 --- /dev/null +++ b/Zsh/.zsh_completion @@ -0,0 +1,175 @@ +# Completion behavior and wrapper bindings + +autoload -Uz add-zsh-hook + +if ! whence -w compdef >/dev/null 2>&1; then + autoload -Uz compinit + compinit -i >/dev/null 2>&1 +fi + +zmodload zsh/complist 2>/dev/null || true + +zstyle ':completion:*' menu select +zstyle ':completion:*' verbose yes +zstyle ':completion:*' list-grouped yes +zstyle ':completion:*:descriptions' format '%F{240}%B%d%b%f' + +typeset -ga DOTZSH_USED_COMMANDS +typeset -ga DOTZSH_ALL_COMMANDS +typeset -gA DOTZSH_USED_COMMAND_MAP +typeset -gA DOTZSH_ALL_COMMAND_MAP +typeset -g DOTZSH_USED_COMMANDS_INITIALIZED=0 +typeset -g DOTZSH_ALL_COMMANDS_INITIALIZED=0 +typeset -g DOTZSH_USED_COMMAND_LIMIT=120 + +_dotzsh_parse_history_line() { + local raw_line="$1" + local parsed_line="$raw_line" + + case "$parsed_line" in + ': '*';'*) + parsed_line="${parsed_line#*;}" + ;; + esac + + print -r -- "$parsed_line" +} + +_dotzsh_extract_command_name() { + local line="$(_dotzsh_parse_history_line "$1")" + local command_name="" + local -a parts=() + local index=1 + + [ -n "$line" ] || return 1 + [[ "$line" == '#'* ]] && return 1 + + parts=(${(z)line}) + [ "${#parts[@]}" -gt 0 ] || return 1 + + while [ "$index" -le "${#parts[@]}" ]; do + command_name="${parts[$index]}" + + case "$command_name" in + sudo|time|command|builtin|noglob|env) + ((index++)) + continue + ;; + *=*) + ((index++)) + continue + ;; + esac + + break + done + + [ -n "$command_name" ] || return 1 + [[ "$command_name" == _* ]] && return 1 + [[ "$command_name" =~ '^[A-Za-z0-9][A-Za-z0-9+._-]*$' ]] || return 1 + + print -r -- "$command_name" +} + +_dotzsh_remember_command() { + local command_name="$1" + + [ -n "$command_name" ] || return 0 + + DOTZSH_USED_COMMANDS=(${DOTZSH_USED_COMMANDS:#$command_name}) + DOTZSH_USED_COMMANDS=("$command_name" "${DOTZSH_USED_COMMANDS[@]}") + DOTZSH_USED_COMMAND_MAP[$command_name]=1 +} + +_dotzsh_initialize_used_commands() { + local history_file="${HISTFILE:-$HOME/.zsh_history}" + local line="" + local command_name="" + + [ "$DOTZSH_USED_COMMANDS_INITIALIZED" -eq 0 ] || return 0 + + DOTZSH_USED_COMMANDS=() + DOTZSH_USED_COMMAND_MAP=() + + while IFS= read -r line; do + command_name="$(_dotzsh_extract_command_name "$line")" || continue + [[ -n ${DOTZSH_USED_COMMAND_MAP[$command_name]:-} ]] && continue + DOTZSH_USED_COMMANDS+=("$command_name") + DOTZSH_USED_COMMAND_MAP[$command_name]=1 + [ "${#DOTZSH_USED_COMMANDS[@]}" -lt "$DOTZSH_USED_COMMAND_LIMIT" ] || break + done < <( + if [ -r "$history_file" ]; then + tac "$history_file" 2>/dev/null || tail -r "$history_file" 2>/dev/null || cat "$history_file" + else + fc -lnr 1 2>/dev/null + fi + ) + + DOTZSH_USED_COMMANDS_INITIALIZED=1 +} + +_dotzsh_initialize_all_commands() { + local command_name="" + + [ "$DOTZSH_ALL_COMMANDS_INITIALIZED" -eq 0 ] || return 0 + + DOTZSH_ALL_COMMANDS=() + DOTZSH_ALL_COMMAND_MAP=() + + for command_name in ${(k)aliases} ${(k)builtins} ${(k)commands} ${(k)functions}; do + [ -n "$command_name" ] || continue + [[ "$command_name" == _* ]] && continue + [[ -n ${DOTZSH_ALL_COMMAND_MAP[$command_name]:-} ]] && continue + DOTZSH_ALL_COMMANDS+=("$command_name") + DOTZSH_ALL_COMMAND_MAP[$command_name]=1 + done + + DOTZSH_ALL_COMMANDS_INITIALIZED=1 +} + +_dotzsh_record_command() { + local command_name="" + + command_name="$(_dotzsh_extract_command_name "$1")" || return 0 + _dotzsh_remember_command "$command_name" +} + +_dotzsh_command_menu() { + local command_name="" + local -a other_commands=() + local -a filtered_used_commands=() + local -a used_display=() + local current_prefix="$PREFIX" + + _dotzsh_initialize_used_commands + _dotzsh_initialize_all_commands + + for command_name in "${DOTZSH_USED_COMMANDS[@]}"; do + [[ -n ${DOTZSH_ALL_COMMAND_MAP[$command_name]:-} ]] || continue + [ -z "$current_prefix" ] || [[ "$command_name" == ${~current_prefix}* ]] || continue + filtered_used_commands+=("$command_name") + used_display+=("* $command_name") + done + + for command_name in "${DOTZSH_ALL_COMMANDS[@]}"; do + [[ -n ${DOTZSH_USED_COMMAND_MAP[$command_name]:-} ]] && continue + [ -z "$current_prefix" ] || [[ "$command_name" == ${~current_prefix}* ]] || continue + other_commands+=("$command_name") + done + + [ "${#filtered_used_commands[@]}" -eq 0 ] || compadd -Q -U -o nosort -d used_display -- "${filtered_used_commands[@]}" + [ "${#other_commands[@]}" -eq 0 ] || compadd -Q -U -o nosort -- "${other_commands[@]}" + + [ "${#filtered_used_commands[@]}" -gt 0 ] || [ "${#other_commands[@]}" -gt 0 ] +} + +compdef _dotzsh_command_menu -command- +add-zsh-hook preexec _dotzsh_record_command + +autoload -Uz _gvm _conda _go _node _npm _npx 2>/dev/null +compdef _gvm gvm +compdef _go go gofmt +compdef _conda conda +compdef _node node +compdef _npm npm +compdef _npx npx diff --git a/Zsh/.zsh_prompt b/Zsh/.zsh_prompt index 860d383..fbd6728 100644 --- a/Zsh/.zsh_prompt +++ b/Zsh/.zsh_prompt @@ -7,6 +7,10 @@ zstyle ':vcs_info:git:*' formats ' %b' typeset -g PROMPT_SHOW_SPACER=0 typeset -gA PROMPT_PROJECT_MATCH_CACHE +typeset -gA PROMPT_VERSION_CACHE +typeset -g PROMPT_FIRST_RENDER=1 +typeset -g PROMPT_PROJECT_ROOT_CACHE_PWD='' +typeset -g PROMPT_PROJECT_ROOT_CACHE='' prompt_preexec() { case "$1" in @@ -24,8 +28,16 @@ get_command_version() { local binary_path="" local raw_version="" + if [[ -n ${PROMPT_VERSION_CACHE[$command_name]+x} ]]; then + print -r -- "${PROMPT_VERSION_CACHE[$command_name]}" + return 0 + fi + binary_path="$(whence -p "$command_name" 2>/dev/null || true)" - [ -n "$binary_path" ] && [ -x "$binary_path" ] || return 0 + if ! [ -n "$binary_path" ] || ! [ -x "$binary_path" ]; then + PROMPT_VERSION_CACHE[$command_name]="" + return 0 + fi case "$command_name" in python) @@ -47,16 +59,29 @@ get_command_version() { raw_version="${raw_version%% *}" ;; *) + PROMPT_VERSION_CACHE[$command_name]="" return 0 ;; esac - [ -n "$raw_version" ] || return 0 + [ -n "$raw_version" ] || { + PROMPT_VERSION_CACHE[$command_name]="" + return 0 + } + + PROMPT_VERSION_CACHE[$command_name]="$raw_version" print -r -- "$raw_version" } get_project_root() { - command git rev-parse --show-toplevel 2>/dev/null || print -r -- "$PWD" + if [ "$PROMPT_PROJECT_ROOT_CACHE_PWD" = "$PWD" ] && [ -n "$PROMPT_PROJECT_ROOT_CACHE" ]; then + print -r -- "$PROMPT_PROJECT_ROOT_CACHE" + return 0 + fi + + PROMPT_PROJECT_ROOT_CACHE_PWD="$PWD" + PROMPT_PROJECT_ROOT_CACHE="$(command git rev-parse --show-toplevel 2>/dev/null || print -r -- "$PWD")" + print -r -- "$PROMPT_PROJECT_ROOT_CACHE" } project_has_pattern() { @@ -166,6 +191,17 @@ build_prompt() { local prompt_spacer="" local separator="::" + if [ "$last_status" -ne 0 ]; then + separator="%F{red}::%f" + fi + + if [ "$PROMPT_FIRST_RENDER" -eq 1 ]; then + PROMPT_FIRST_RENDER=0 + PROMPT="%B%~%b ${separator} " + RPROMPT="%n@%m" + return 0 + fi + vcs_info git_segment="${vcs_info_msg_0_}" git_status_segment="$(build_git_status_segment)" @@ -228,10 +264,6 @@ build_prompt() { context_line+="${newline}" fi - if [ "$last_status" -ne 0 ]; then - separator="%F{red}::%f" - fi - if [ "$PROMPT_SHOW_SPACER" -eq 1 ]; then prompt_spacer="$(build_prompt_separator_line "$last_status")${newline}" fi diff --git a/Zsh/.zshrc b/Zsh/.zshrc index 4db1ba6..97c0f06 100644 --- a/Zsh/.zshrc +++ b/Zsh/.zshrc @@ -7,14 +7,23 @@ export ZSH="$HOME/.oh-my-zsh" export PROG_DIR="$HOME/.programming" # Plugins -plugins=(git zsh-syntax-highlighting zsh-autosuggestions sudo rclone rust nvm golang conda pyenv) +plugins=(git zsh-syntax-highlighting zsh-autosuggestions sudo rclone) source $ZSH/oh-my-zsh.sh # Go and GVM (Black Box) export GOPATH="$PROG_DIR/go" export GVM_ROOT="$GOPATH" -[[ -s "$GVM_ROOT/scripts/gvm" ]] && source "$GVM_ROOT/scripts/gvm" + +gvm_load() { + unset -f gvm go gofmt + [[ -s "$GVM_ROOT/scripts/gvm" ]] && source "$GVM_ROOT/scripts/gvm" + "$@" +} + +gvm() { gvm_load gvm "$@"; } +go() { gvm_load go "$@"; } +gofmt() { gvm_load gofmt "$@"; } # Node and NVM (Lazy Load) export NVM_DIR="$PROG_DIR/node" @@ -37,16 +46,25 @@ export CARGO_HOME="$PROG_DIR/rust/cargo" # Python (Pyenv + Miniconda) export PYENV_ROOT="$PROG_DIR/python/pyenv" -export PATH="$PYENV_ROOT/bin:$PATH" +export PATH="$PYENV_ROOT/bin:$PYENV_ROOT/shims:$PATH" +export CONDA_ROOT="$PYENV_ROOT/versions/miniforge3-latest" -if command -v pyenv 1>/dev/null 2>&1; then - eval "$(pyenv init -)" -fi +pyenv_load() { + unset -f pyenv + eval "$(command pyenv init -)" + pyenv "$@" +} -if command -v conda >/dev/null 2>&1; then - CONDA_BASE=$(conda info --base) - [ -f "$CONDA_BASE/etc/profile.d/conda.sh" ] && source "$CONDA_BASE/etc/profile.d/conda.sh" -fi +pyenv() { pyenv_load "$@"; } + +conda_load() { + unset -f conda mamba + [ -f "$CONDA_ROOT/etc/profile.d/conda.sh" ] && source "$CONDA_ROOT/etc/profile.d/conda.sh" + "$@" +} + +conda() { conda_load conda "$@"; } +mamba() { conda_load mamba "$@"; } # R and Rig export RIG_HOME="$PROG_DIR/r" @@ -54,6 +72,10 @@ if [ -d "$RIG_HOME/bin" ]; then export PATH="$RIG_HOME/bin:$PATH" fi +is_wsl() { + [ -f /proc/version ] && grep -qEi "(Microsoft|WSL)" /proc/version +} + # Zoxide if command -v zoxide >/dev/null 2>&1; then eval "$(zoxide init --cmd cd zsh)" @@ -67,8 +89,8 @@ fi export PATH="$HOME/.local/bin:$CARGO_HOME/bin:$GOPATH/bin:$PATH" # opencode -if command -v openchamber >/dev/null 2>&1 && ! pgrep -f "openchamber.*7891" > /dev/null; then - openchamber --port 7891 >/dev/null 2>&1 +if is_wsl && command -v openchamber >/dev/null 2>&1 && ! pgrep -f "openchamber.*7891" > /dev/null; then + openchamber --port 7891 >/dev/null 2>&1 &! fi # direnv @@ -76,5 +98,8 @@ if command -v direnv >/dev/null 2>&1; then eval "$(direnv hook zsh)" fi +# Completion Styling +[ -f ~/.zsh_completion ] && source ~/.zsh_completion + # Prompt Styling [ -f ~/.zsh_prompt ] && source ~/.zsh_prompt