From ffd26e06d6974567637cdf2b8afe59084e2167b7 Mon Sep 17 00:00:00 2001 From: Andrejus Date: Tue, 24 Mar 2026 18:08:40 +0000 Subject: [PATCH] perf(zsh): cache tool init, lazy-load, remove plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Startup: 150ms → 40ms (native), projected 10-30s → <1s (iSH) - Cache mise/fzf/zoxide init output keyed on binary mtime - zcompile all sourced files to bytecode at install time - Lazy-load compinit (first Tab), fzf+widgets (first keystroke) - Remove autosuggestions and syntax-highlighting plugins - Switch mise to shims mode (no per-prompt hook) - Conditional mkdir (skip if dirs exist) - Remove TRAPINT/flash handler, cache session info - Eliminate per-prompt subshell forks (REPLY pattern) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- home/.zsh/prompt.zsh | 49 +++++------------ home/.zshrc | 123 ++++++++++++++++++++++++------------------- install.d/22-zsh.sh | 23 +------- install.d/23-stow.sh | 17 ++++-- 4 files changed, 98 insertions(+), 114 deletions(-) diff --git a/home/.zsh/prompt.zsh b/home/.zsh/prompt.zsh index 2cb3cf0..62dd697 100644 --- a/home/.zsh/prompt.zsh +++ b/home/.zsh/prompt.zsh @@ -1,12 +1,11 @@ # Prompt (( ${+PROMPT_MIN_DURATION} )) || typeset -gi PROMPT_MIN_DURATION=2 # show duration after N seconds -(( ${+PROMPT_FLASH_DELAY} )) || typeset -gi PROMPT_FLASH_DELAY=4 # flash prompt for N centiseconds typeset -gi _dots_prompt_cmd_start=0 typeset -gi _dots_prompt_cmd_ran=0 -typeset -gi _dots_prompt_flashing=0 typeset -g _dots_prompt_symbol="λ" typeset -g _dots_prompt_base="" +typeset -g _dots_session_cache="" typeset -gA _dots_pc _dots_init_colors() { @@ -59,14 +58,17 @@ _dots_abbrev_path() { local -a parts=( "${(@s:/:)dir}" ) local count=${#parts[@]} - (( count <= 3 )) && { print -r -- "$dir"; return } + if (( count <= 3 )); then + REPLY="$dir" + return + fi local result="" local i for (( i=1; i <= count-3; i++ )); do result+="${parts[i][1]}/" done - print -r -- "${result}${parts[-3]}/${parts[-2]}/${parts[-1]}" + REPLY="${result}${parts[-3]}/${parts[-2]}/${parts[-1]}" } _dots_session() { @@ -189,18 +191,15 @@ typeset -g _dots_git_async_fd="" _dots_git_async_callback() { local fd=$1 local result="" - # Use sysread for efficient non-blocking read from fd if [[ -n "$fd" ]] && sysread -i "$fd" result 2>/dev/null; then - result="${result%$'\n'}" # trim trailing newline + result="${result%$'\n'}" _dots_git_info_result="$result" _dots_build_dots_prompt_base PROMPT="$_dots_prompt_base" - # Only reset prompt if not in a special ZLE widget (e.g. fzf) if zle && [[ "${WIDGET:-}" != _dots_* ]]; then zle reset-prompt 2>/dev/null fi fi - # Clean up exec {fd}<&- zle -F "$fd" 2>/dev/null _dots_git_async_fd="" @@ -226,7 +225,8 @@ _dots_git_async_start() { } _dots_build_dots_prompt_base() { - local dir_path="$(_dots_abbrev_path)" + _dots_abbrev_path + local dir_path="$REPLY" local symbol="${_dots_pc[grey]}${_dots_prompt_symbol}${_dots_pc[reset]}" (( EUID == 0 )) && symbol="${_dots_pc[orange]}${_dots_pc[bold]}#${_dots_pc[reset]}" @@ -264,7 +264,7 @@ _dots_precmd() { (( e )) && rp_parts+=("${_dots_pc[orange]}[${e}]${_dots_pc[reset]}") - local session="$(_dots_session)" + local session="$_dots_session_cache" [[ -n "$session" ]] && rp_parts+=("${_dots_pc[dark_bg]}${_dots_pc[dark]}[${_dots_pc[orange]}${session}${_dots_pc[reset]}${_dots_pc[dark_bg]}${_dots_pc[dark]}]${_dots_pc[reset]}") RPROMPT="${(j: :)rp_parts}" @@ -282,36 +282,11 @@ _dots_precmd() { -TRAPINT() { - # Only customize when ZLE is active (at prompt, not during command) - if [[ -o zle ]] && [[ -o interactive ]] && (( ${+WIDGET} )); then - if [[ -z "$BUFFER" ]] && (( ! _dots_prompt_flashing )); then - # Empty buffer: flash the prompt symbol - _dots_prompt_flashing=1 - local git_part="" - [[ -n "$_dots_git_info_result" ]] && git_part=" ${_dots_git_info_result}" - local flash_prompt=$'\n'"${_dots_pc[dark_bg]}${_dots_pc[dark]}#${_dots_pc[teal]}$(_dots_abbrev_path)${_dots_pc[reset]}${_dots_pc[dark_bg]}${_dots_pc[dark]}#${_dots_pc[reset]}${git_part}"$'\n'$'%{\e[48;2;248;140;20m\e[30m%}'"${_dots_prompt_symbol}"$' %{\e[0m%}' - PROMPT="$flash_prompt" - zle reset-prompt - zselect -t $PROMPT_FLASH_DELAY - _dots_prompt_flashing=0 - PROMPT="$_dots_prompt_base" - zle reset-prompt - return 0 - elif [[ -n "$BUFFER" ]]; then - # Buffer has content: clear autosuggest, then default behavior - zle autosuggest-clear 2>/dev/null - fi - fi - # Propagate signal: use special return code -1 to let zsh handle normally - return $((128 + ${1:-2})) -} - _dots_prompt_init() { zmodload zsh/datetime 2>/dev/null - zmodload zsh/zselect 2>/dev/null zmodload zsh/system 2>/dev/null _dots_init_colors + _dots_session_cache="$(_dots_session)" _dots_build_dots_prompt_base setopt PROMPT_SUBST EXTENDED_HISTORY INC_APPEND_HISTORY_TIME @@ -319,7 +294,7 @@ _dots_prompt_init() { add-zsh-hook preexec _dots_preexec add-zsh-hook precmd _dots_precmd add-zsh-hook chpwd _dots_build_dots_prompt_base - + PROMPT="$_dots_prompt_base" RPROMPT="" } _dots_prompt_init diff --git a/home/.zshrc b/home/.zshrc index 3d299cd..0e0feb6 100644 --- a/home/.zshrc +++ b/home/.zshrc @@ -1,19 +1,38 @@ # Profiling: ZSH_BENCH=1 zsh [[ -n "$ZSH_BENCH" ]] && zmodload zsh/zprof -# Upgrade xterm-color to xterm-256color (gh cs ssh sets the weaker value) +# Terminal capabilities [[ "$TERM" == "xterm-color" ]] && export TERM=xterm-256color - -# Assume truecolor support if terminal advertises 256color (covers SSH, tmux) [[ -z "$COLORTERM" && "$TERM" == *256color* ]] && export COLORTERM=truecolor _dots_cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/dots" +# Cache eval output keyed on binary mtime — busts on brew upgrade / tool update +_dots_cached_eval() { + local name="$1" bin="$2"; shift 2 + local cache="$_dots_cache_dir/${name}.zsh" + if [[ -f "$cache" && "$cache" -nt "$bin" ]]; then + source "$cache" + else + "$@" > "$cache" 2>/dev/null + if [[ -s "$cache" ]]; then + zcompile "$cache" 2>/dev/null + source "$cache" + else + rm -f "$cache"; return 1 + fi + fi +} + +# --- Environment --- + _dots_load_profile() { source "$HOME/.profile" } _dots_load_profile _dots_setup_dirs() { - mkdir -p "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" "$HOME/.local/bin" "$WORKSPACE" "$_dots_cache_dir" + local d; for d in "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" "$HOME/.local/bin" "$WORKSPACE" "$_dots_cache_dir"; do + [[ -d "$d" ]] || mkdir -p "$d" + done } _dots_setup_dirs @@ -37,12 +56,13 @@ _dots_cache_ls_colors [[ -f ~/.aliases ]] && source ~/.aliases +# --- Completion (lazy — deferred until first Tab press) --- + _dots_init_completion() { local comp_dir="${XDG_DATA_HOME:-$HOME/.local/share}/zsh/completions" [[ -d "$comp_dir" ]] && fpath=("$comp_dir" $fpath) autoload -Uz compinit - # Daily cache invalidation local dump="$HOME/.zcompdump" if [[ -f "$dump" ]]; then zmodload -F zsh/stat b:zstat 2>/dev/null @@ -57,7 +77,6 @@ _dots_init_completion() { compinit fi - # Completion styling zstyle ':completion:*' menu select zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}" zstyle ':completion:*' group-name '' @@ -65,40 +84,22 @@ _dots_init_completion() { zstyle ':completion:*:warnings' format $'\e[38;2;248;140;20m-- no matches --\e[0m' zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}' } -_dots_init_completion -_dots_load_plugins() { - local plugin_dir="${XDG_DATA_HOME:-$HOME/.local/share}/zsh/plugins" - - ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=#3C3C3C' - - local f="$plugin_dir/zsh-autosuggestions/zsh-autosuggestions.zsh" - [[ -f "$f" ]] && source "$f" - - # syntax-highlighting must be sourced last - f="$plugin_dir/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" - [[ -f "$f" ]] && source "$f" - - # Syntax highlighting theme - typeset -gA ZSH_HIGHLIGHT_STYLES - ZSH_HIGHLIGHT_STYLES[command]='fg=#2CB494' - ZSH_HIGHLIGHT_STYLES[builtin]='fg=#2CB494' - ZSH_HIGHLIGHT_STYLES[alias]='fg=#2CB494' - ZSH_HIGHLIGHT_STYLES[function]='fg=#2CB494' - ZSH_HIGHLIGHT_STYLES[unknown-token]='fg=#F88C14' - ZSH_HIGHLIGHT_STYLES[path]='fg=#CCE0D0,underline' - ZSH_HIGHLIGHT_STYLES[globbing]='fg=#F88C14' - ZSH_HIGHLIGHT_STYLES[single-quoted-argument]='fg=#7290B8' - ZSH_HIGHLIGHT_STYLES[double-quoted-argument]='fg=#7290B8' - ZSH_HIGHLIGHT_STYLES[dollar-quoted-argument]='fg=#7290B8' - ZSH_HIGHLIGHT_STYLES[comment]='fg=#808080' - ZSH_HIGHLIGHT_STYLES[arg0]='fg=#2CB494' - ZSH_HIGHLIGHT_STYLES[default]='fg=#CCE0D0' - ZSH_HIGHLIGHT_STYLES[commandseparator]='fg=#808080' - ZSH_HIGHLIGHT_STYLES[redirection]='fg=#F88C14' - ZSH_HIGHLIGHT_STYLES[option]='fg=#7290B8' +# Stub that loads real completion on first Tab, then replays the keypress +_dots_lazy_comp_widget() { + _dots_init_completion + zle -D _dots_lazy_comp_widget + # If fzf-completion exists (loaded via zle-line-init), use it; otherwise default + if (( ${+widgets[fzf-completion]} )); then + zle fzf-completion "$@" + else + zle expand-or-complete "$@" + fi } -_dots_load_plugins +zle -N _dots_lazy_comp_widget +bindkey '^I' _dots_lazy_comp_widget + +# --- History & options --- _dots_load_history() { HISTFILE="${XDG_DATA_HOME:-$HOME/.local/share}/zsh/history" @@ -111,22 +112,31 @@ _dots_load_history setopt IGNORE_EOF -source "$HOME/.zsh/widgets.zsh" +# --- Tool init (cached) --- -_dots_load_fzf() { - command -v fzf &>/dev/null || return +_dots_load_mise() { + local bin="${commands[mise]:-}" + [[ -n "$bin" ]] || return + _dots_cached_eval mise "$bin" mise activate --shims zsh +} + +# fzf env vars (needed by widgets and zoxide before fzf init loads) +if [[ -n "${commands[fzf]:-}" ]]; then export FZF_DEFAULT_COMMAND='rg --files --hidden --glob "!.git"' export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" export FZF_DEFAULT_OPTS='--style=minimal --layout=reverse --height=40% --border=none --no-scrollbar --prompt="> " --info=inline-right --no-separator --margin=1,0,0,0 --color=fg:#808080,fg+:#CCE0D0,bg:-1,bg+:#1A1A1A --color=hl:#2CB494,hl+:#2CB494,info:#808080,marker:#2CB494 --color=prompt:#2CB494,spinner:#88409C,pointer:#2CB494,header:#808080 --color=border:#3C3C3C,preview-border:#3C3C3C,gutter:#1A1A1A,preview-fg:#CCE0D0' - # fzf --zsh requires v0.48+ - if fzf --zsh &>/dev/null; then - source <(fzf --zsh) - else +fi + +_dots_load_fzf() { + local bin="${commands[fzf]:-}" + [[ -n "$bin" ]] || return + if ! _dots_cached_eval fzf "$bin" fzf --zsh; then local -a fzf_paths=( "${HOMEBREW_PREFIX:-/opt/homebrew}/opt/fzf/shell" "/usr/share/fzf" "${XDG_DATA_HOME:-$HOME/.local/share}/fzf/shell" ) + local dir for dir in "${fzf_paths[@]}"; do [[ -f "$dir/key-bindings.zsh" ]] && source "$dir/key-bindings.zsh" && break done @@ -136,21 +146,28 @@ _dots_load_fzf() { fi } - _dots_load_zoxide() { - command -v zoxide &>/dev/null || return + local bin="${commands[zoxide]:-}" + [[ -n "$bin" ]] || return export _ZO_FZF_OPTS="$FZF_DEFAULT_OPTS" export _ZO_ECHO=0 - eval "$(zoxide init zsh)" + _dots_cached_eval zoxide "$bin" zoxide init zsh } +_dots_load_mise +_dots_load_zoxide + +# --- Interactive shell --- + source "$HOME/.zsh/prompt.zsh" -_dots_load_mise() { - command -v mise &>/dev/null && eval "$(mise activate zsh)" +# Load fzf + widgets after first prompt renders (zle-line-init fires before first keystroke) +autoload -Uz add-zle-hook-widget +_dots_lazy_widgets() { + _dots_load_fzf + source "$HOME/.zsh/widgets.zsh" + add-zle-hook-widget -d zle-line-init _dots_lazy_widgets } -_dots_load_mise -_dots_load_fzf -_dots_load_zoxide +add-zle-hook-widget zle-line-init _dots_lazy_widgets [[ -n "$ZSH_BENCH" ]] && zprof || true diff --git a/install.d/22-zsh.sh b/install.d/22-zsh.sh index 1821830..f480797 100755 --- a/install.d/22-zsh.sh +++ b/install.d/22-zsh.sh @@ -2,7 +2,7 @@ # ----------------------------------------------------------------------------- # Description: -# Configure zsh shell with direct plugin management. +# Configure zsh shell. # # install zsh @@ -24,26 +24,6 @@ if ! command -v zsh &> /dev/null; then esac fi -zsh --version | log_quote - -# plugin directory (XDG compliant) -PLUGIN_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/zsh/plugins" -mkdir -p "$PLUGIN_DIR" - -# install zsh-autosuggestions -if [ ! -d "$PLUGIN_DIR/zsh-autosuggestions" ]; then - git clone -q \ - https://github.com/zsh-users/zsh-autosuggestions.git \ - "$PLUGIN_DIR/zsh-autosuggestions" -fi - -# install zsh-syntax-highlighting -if [ ! -d "$PLUGIN_DIR/zsh-syntax-highlighting" ]; then - git clone -q \ - https://github.com/zsh-users/zsh-syntax-highlighting.git \ - "$PLUGIN_DIR/zsh-syntax-highlighting" -fi - # change default shell to zsh if [[ "$SHELL" != *zsh ]]; then sudo chsh -s "$(command -v zsh)" "$(whoami)" @@ -51,4 +31,5 @@ if [[ "$SHELL" != *zsh ]]; then fi log_pass "zsh configured" +zsh --version | log_quote diff --git a/install.d/23-stow.sh b/install.d/23-stow.sh index d6c83bb..567bd6c 100755 --- a/install.d/23-stow.sh +++ b/install.d/23-stow.sh @@ -23,8 +23,6 @@ if ! command -v stow &> /dev/null; then esac fi -stow --version | log_quote - root_dir=${DOTFILES:-$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")} rm -f "$HOME/.bash_profile" @@ -49,5 +47,18 @@ fi # Bust PATH cache to force rebuild with new profile rm -f "${XDG_CACHE_HOME:-$HOME/.cache}/dots/path" -log_pass "stow linked" +# Compile zsh dotfiles for faster shell startup +if command -v zsh &>/dev/null; then + zsh -c ' + for f in ~/.zsh/*.zsh ~/.aliases ~/.profile(N); do + [[ $f.zwc -nt $f ]] || zcompile "$f" 2>/dev/null + done + ' +fi + +# Bust tool init caches so they regenerate with new PATH/tools +rm -f "${XDG_CACHE_HOME:-$HOME/.cache}"/dots/{fzf,mise,zoxide}.zsh{,.zwc} + +log_pass "stow linked" +stow --version | log_quote