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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@
[submodule "zsh-plugins/zsh-autosuggestions"]
path = zsh-plugins/zsh-autosuggestions
url = git@github.com:zsh-users/zsh-autosuggestions.git
[submodule "zsh-plugins/zsh-defer"]
path = zsh-plugins/zsh-defer
url = https://github.com/romkatv/zsh-defer.git
23 changes: 18 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ Personal Zsh configuration repository: shell environment, dotfiles, aliases, fun
### Loading Chain
1. `~/.zshrc` sources `load.zsh` (added by `install.sh`)
2. `load.zsh` orchestrates everything:
- Locale, zsh options, plugins (git submodules), theme
- Locale, zsh options, `path-ethic` plugin, theme (synchronous — needed at startup)
- Sources all `include/` files: exports, aliases, functions, keybindings, completions, history, prompt
- **`zsh-syntax-highlighting` MUST be loaded LAST** (after all other plugins and keybindings)
- **Deferred plugins** (via `zsh-defer`, interactive sessions only): `zsh-history-substring-search`, `zsh-autosuggestions`, `zsh-syntax-highlighting` — these load after the prompt appears
- **Non-interactive fallback**: deferred plugins load synchronously when `[[ ! -o interactive ]]` (e.g., tests)
- **`zsh-syntax-highlighting` MUST be loaded LAST** (after all other plugins and keybindings, even when deferred)
- **`fpath` must include `zsh-completions/src` BEFORE `autoload`**
- **History-substring-search keybindings** are deferred in `load.zsh` (not in `include/keybindings`) because they depend on the deferred plugin

### Key Directories
- **`include/`** — Modular shell config files sourced by `load.zsh`
Expand All @@ -27,10 +30,14 @@ Personal Zsh configuration repository: shell environment, dotfiles, aliases, fun
## Commands

```bash
./install.sh # Full setup: submodules, symlinks, dirs, .zshrc, neovim
./install.sh # Full setup: submodules, symlinks, dirs, .zshrc, neovim, zwc compilation
make test # Run tests (also: ./tests/run_tests.sh)
make update_submodules # Update all git submodules
reload! # Alias to re-source ~/.zshrc (use after editing include/ files)
make compile # Compile zsh files to .zwc bytecode
make clean_zwc # Remove all .zwc bytecode files
reload! # Alias: recompiles .zwc files, then re-sources ~/.zshrc
zsh_bench # Benchmark shell startup time (see scripts/zsh_bench)
zsh_bench -n 20 -o # 20 iterations, save results to .benchmarks/
```

To run a single test file directly: `zsh tests/my_test.test.sh` (but prefer `make test` — the runner sets up the sandbox environment via `zsh-scriptest`).
Expand Down Expand Up @@ -73,7 +80,13 @@ When modifying `include/aliases`:
- Other env vars: `include/exports`

### Scripts as CLI Commands
Files in `scripts/` are on `$PATH` and act as standalone commands. Notable: `y` and `n` (yarn/npm wrappers), `docker_cleanup`, `git_config_hook`. New scripts placed here are automatically available as commands.
Files in `scripts/` are on `$PATH` and act as standalone commands. Notable: `y` and `n` (yarn/npm wrappers), `docker_cleanup`, `git_config_hook`, `zsh_bench` (startup benchmarking). New scripts placed here are automatically available as commands.

### Performance
- **Startup time**: Optimized via `compinit` caching, `.zwc` bytecode compilation, and `zsh-defer` plugin deferral. Use `zsh_bench` to measure impact of changes.
- **compinit caching** (`include/completions`): The `.zcompdump` file is only regenerated when stale (>24h) or missing. `compinit -C` is used otherwise.
- **`.zwc` bytecode**: Zsh auto-prefers `file.zwc` over `file` when sourcing (if `.zwc` is newer). Stale `.zwc` files are harmless — zsh falls back to the source. `reload!` and `install.sh` recompile automatically. Some files (aliases, exports, functions) fail `zcompile` silently — this is expected.
- **`zsh-defer`**: Only used in interactive sessions. Keybindings that depend on deferred plugins must also be deferred — see `load.zsh` for the pattern.

### Cross-Platform
Changes must work on both macOS and Linux. Use platform checks (`$OSTYPE`) when behavior differs (see `__profile_git_file_timestamp` for an example).
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,13 @@ test:

update_submodules:
git submodule update --recursive --remote

compile:
@echo "Compiling zsh files to zwc bytecode..."
@zsh -c 'for f in $(BASEDIR)/load.zsh $(BASEDIR)/include/*(.) $(BASEDIR)/scripts/lib.zsh; do [[ $$f == *.zwc ]] && continue; zcompile $$f 2>/dev/null; done'
@echo "Done."

clean_zwc:
@echo "Removing zwc bytecode files..."
@find $(BASEDIR) -name "*.zwc" -delete
@echo "Done."
4 changes: 4 additions & 0 deletions dotfiles/.gitignore_global
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,7 @@ vendor/
*.log
*.jfr

.benchmarks/

# Zsh bytecode compiled files
*.zwc
2 changes: 1 addition & 1 deletion include/aliases
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# profile self update
alias update_profile="git -C $SHA1N_PROFILE_HOME pull --recurse-submodules origin $(git -C $SHA1N_PROFILE_HOME branch --show-current)"
alias reload!='source $HOME/.zshrc'
alias reload!='for f in $SHA1N_PROFILE_HOME/load.zsh $SHA1N_PROFILE_HOME/include/*(.) $SHA1N_PROFILE_HOME/scripts/lib.zsh; do [[ $f == *.zwc ]] && continue; zcompile $f 2>/dev/null; done; source $HOME/.zshrc'
#
# directory shortcuts
#
Expand Down
7 changes: 6 additions & 1 deletion include/completions
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,10 @@ zstyle ':completion:*:default' list-colors \
"di=1;36" "ln=1;35" "so=32" "pi=33" "ex=31" "bd=34;46" "cd=34;43" \
"su=30;41" "sg=30;46" "tw=30;42" "ow=30;43"

autoload -U compinit && compinit
autoload -U compinit
if [[ -n ~/.zcompdump(#qN.mh+24) ]] || [[ ! -f ~/.zcompdump ]]; then
compinit
else
compinit -C
fi
autoload -U +X bashcompinit && bashcompinit
6 changes: 3 additions & 3 deletions include/keybindings
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env zsh

# history-substring-search key bindings
bindkey "^[[A" history-substring-search-up
bindkey "^[[B" history-substring-search-down
# history-substring-search key bindings are deferred in load.zsh
# (they depend on the zsh-history-substring-search plugin which is also deferred)

# line navigation
bindkey "^[[H" beginning-of-line
bindkey "^[[F" end-of-line
Expand Down
7 changes: 7 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,11 @@ validate_shell_rc_file && install_source_command

setup_neovim

__profile_log_info "compiling zsh files to bytecode..."
for f in "$SHA1N_PROFILE_HOME"/load.zsh "$SHA1N_PROFILE_HOME"/include/*(.) "$SHA1N_PROFILE_HOME"/scripts/lib.zsh; do
[[ "$f" == *.zwc ]] && continue
zcompile "$f" 2>/dev/null
done
__profile_log_success "bytecode compilation complete"

__profile_log_info "done!"
26 changes: 23 additions & 3 deletions load.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,17 @@ export LC_CTYPE="en_US.UTF-8"
# plugins
fpath=($SHA1N_PROFILE_HOME/zsh-plugins/zsh-completions/src $fpath)
source $SHA1N_PROFILE_HOME/zsh-plugins/path-ethic/path-ethic.plugin.zsh
source $SHA1N_PROFILE_HOME/zsh-plugins/zsh-history-substring-search/zsh-history-substring-search.zsh
source $SHA1N_PROFILE_HOME/zsh-plugins/zsh-autosuggestions/zsh-autosuggestions.zsh

# zsh-defer for deferring non-critical interactive plugins
if [[ -o interactive ]]; then
source $SHA1N_PROFILE_HOME/zsh-plugins/zsh-defer/zsh-defer.plugin.zsh
zsh-defer source $SHA1N_PROFILE_HOME/zsh-plugins/zsh-history-substring-search/zsh-history-substring-search.zsh
zsh-defer source $SHA1N_PROFILE_HOME/zsh-plugins/zsh-autosuggestions/zsh-autosuggestions.zsh
else
source $SHA1N_PROFILE_HOME/zsh-plugins/zsh-history-substring-search/zsh-history-substring-search.zsh
source $SHA1N_PROFILE_HOME/zsh-plugins/zsh-autosuggestions/zsh-autosuggestions.zsh
fi

# theme
source $SHA1N_PROFILE_HOME/zsh-theme/agnoster-zsh-theme/agnoster.zsh-theme

Expand All @@ -25,4 +34,15 @@ source $SHA1N_PROFILE_HOME/include/completions
source $SHA1N_PROFILE_HOME/include/history
source $SHA1N_PROFILE_HOME/include/prompt

source $SHA1N_PROFILE_HOME/zsh-plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
# deferred keybindings that depend on history-substring-search
if [[ -o interactive ]]; then
zsh-defer bindkey "^[[A" history-substring-search-up
zsh-defer bindkey "^[[B" history-substring-search-down
fi

# zsh-syntax-highlighting MUST be loaded LAST
if [[ -o interactive ]]; then
zsh-defer source $SHA1N_PROFILE_HOME/zsh-plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
else
source $SHA1N_PROFILE_HOME/zsh-plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
fi
81 changes: 81 additions & 0 deletions scripts/zsh_bench
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env zsh

# zsh_bench - Measure zsh interactive shell startup time
#
# Usage: zsh_bench [-n iterations] [-o]
# -n Number of iterations (default: 10)
# -o Save results to .benchmarks/

zmodload zsh/datetime

local iterations=10
local save=false
local script_dir="${${(%):-%x}:a:h}"
local repo_dir="${script_dir:h}"
local benchmarks_dir="$repo_dir/.benchmarks"

while getopts "n:o" opt; do
case $opt in
n) iterations=$OPTARG ;;
o) save=true ;;
\?) echo "Usage: zsh_bench [-n iterations] [-o]" >&2; return 1 ;;
esac
done

echo "Running $iterations iterations...\n"

local -a times
for i in $(seq 1 $iterations); do
local start=$EPOCHREALTIME
zsh -i -c exit 2>/dev/null
local end=$EPOCHREALTIME
local elapsed=$(( (end - start) * 1000 ))
times+=($elapsed)
printf " #%2d %6.1f ms\n" "$i" "$elapsed"
done

# Sort times
local -a sorted
sorted=(${(on)times})

local min=${sorted[1]}
local max=${sorted[-1]}
local median
local mid=$(( iterations / 2 ))
if (( iterations % 2 == 0 )); then
median=$(( (sorted[mid] + sorted[mid + 1]) / 2.0 ))
else
median=${sorted[mid + 1]}
fi

local sum=0
for t in "${times[@]}"; do
sum=$(( sum + t ))
done
local avg=$(( sum / iterations ))

echo ""
echo "Results:"
echo " min: $(printf '%6.1f' $min) ms"
echo " max: $(printf '%6.1f' $max) ms"
echo " avg: $(printf '%6.1f' $avg) ms"
echo " median: $(printf '%6.1f' $median) ms"

if $save; then
mkdir -p "$benchmarks_dir"
local timestamp=$(date +%Y%m%d_%H%M%S)
local outfile="$benchmarks_dir/${timestamp}.txt"
{
echo "zsh_bench — $iterations iterations — $(date)"
echo ""
for i in $(seq 1 $iterations); do
printf " #%2d %6.1f ms\n" "$i" "${times[$i]}"
done
echo ""
echo "min: $(printf '%6.1f' $min) ms"
echo "max: $(printf '%6.1f' $max) ms"
echo "avg: $(printf '%6.1f' $avg) ms"
echo "median: $(printf '%6.1f' $median) ms"
} > "$outfile"
echo "\nResults saved to $outfile"
fi
1 change: 1 addition & 0 deletions zsh-plugins/zsh-defer
Submodule zsh-defer added at 53a26e
Loading