| name | shell-prompt |
| description | Modern shell prompt configuration with Powerlevel10k and Zsh Vi Mode. Use when configuring shell prompts, setting up vi/vim keybindings in zsh, customizing cursor styles per mode, adding mode indicators, optimizing prompt performance, or troubleshooting slow prompts. Covers P10k instant prompt, vi mode plugins, and cursor customization. |
Shell Prompt Skill
Configure high-performance shell prompts with Powerlevel10k and Zsh Vi Mode.
Overview
Modern shell prompts provide:
- Git status with branch, dirty state, and remote tracking
- Environment indicators (Python venv, Node version, K8s context)
- Execution time for long-running commands
- Exit code visualization
- Async updates for responsive experience
- Vi mode indicators and cursor changes
Zsh Vi Mode
Zsh supports vi-style line editing with visual feedback through cursor changes and mode indicators.
Quick Setup (Built-in)
bindkey -v
export KEYTIMEOUT=10
Cursor Style by Mode
Change cursor shape based on current mode:
cursor_mode() {
cursor_beam='\e[6 q'
cursor_block='\e[2 q'
function zle-keymap-select {
if [[ ${KEYMAP} == vicmd ]] ||
[[ $1 = 'block' ]]; then
echo -ne $cursor_block
elif [[ ${KEYMAP} == main ]] ||
[[ ${KEYMAP} == viins ]] ||
[[ ${KEYMAP} = '' ]] ||
[[ $1 = 'beam' ]]; then
echo -ne $cursor_beam
fi
}
zle-line-init() {
echo -ne $cursor_beam
}
zle -N zle-keymap-select
zle -N zle-line-init
}
cursor_mode
Cursor Escape Codes
| Code | Style |
|---|
\e[1 q | Blinking block |
\e[2 q | Steady block |
\e[3 q | Blinking underline |
\e[4 q | Steady underline |
\e[5 q | Blinking bar/beam |
\e[6 q | Steady bar/beam |
Vi Mode Plugins
Oh My Zsh vi-mode Plugin
plugins=(... vi-mode)
VI_MODE_SET_CURSOR=true
VI_MODE_RESET_PROMPT_ON_MODE_CHANGE=true
VI_MODE_CURSOR_NORMAL=2
VI_MODE_CURSOR_INSERT=6
VI_MODE_CURSOR_VISUAL=6
VI_MODE_CURSOR_OPPEND=0
MODE_INDICATOR="%F{red}<<<NORMAL%f"
INSERT_MODE_INDICATOR="%F{green}<<<INSERT%f"
softmoth/zsh-vim-mode
Full-featured vi mode with text objects and surround bindings.
Installation:
git clone https://github.com/softmoth/zsh-vim-mode.git ~/.zsh/zsh-vim-mode
source ~/.zsh/zsh-vim-mode/zsh-vim-mode.plugin.zsh
Load order matters: zsh-autosuggestions -> zsh-syntax-highlighting -> zsh-vim-mode
Configuration:
MODE_CURSOR_VIINS="#00ff00 blinking bar"
MODE_CURSOR_VICMD="green block"
MODE_CURSOR_REPLACE="red block"
MODE_CURSOR_SEARCH="#ff00ff steady underline"
MODE_CURSOR_VISUAL="$MODE_CURSOR_VICMD steady bar"
MODE_CURSOR_VLINE="$MODE_CURSOR_VISUAL #00ffff"
MODE_INDICATOR_VIINS='%F{15}<%F{8}INSERT>%f'
MODE_INDICATOR_VICMD='%F{10}<%F{2}NORMAL>%f'
MODE_INDICATOR_REPLACE='%F{9}<%F{1}REPLACE>%f'
MODE_INDICATOR_SEARCH='%F{13}<%F{5}SEARCH>%f'
MODE_INDICATOR_VISUAL='%F{12}<%F{4}VISUAL>%f'
MODE_INDICATOR_VLINE='%F{12}<%F{4}V-LINE>%f'
VIM_MODE_VICMD_KEY='^['
VIM_MODE_TRACK_KEYMAP=true
VIM_MODE_INITIAL_KEYMAP=viins
Features:
- Text objects:
ci", da(, vi[
- Surround:
cs"' (change surrounding " to ')
- Visual mode selection
- Emacs bindings in insert mode (Ctrl-A, Ctrl-E)
jeffreytse/zsh-vi-mode
Modern vi mode with operator-pending mode support.
Installation:
zinit ice depth=1
zinit light jeffreytse/zsh-vi-mode
git clone https://github.com/jeffreytse/zsh-vi-mode.git ~/.zsh/zsh-vi-mode
source ~/.zsh/zsh-vi-mode/zsh-vi-mode.plugin.zsh
Configuration:
ZVM_NORMAL_MODE_CURSOR=$ZVM_CURSOR_BLOCK
ZVM_INSERT_MODE_CURSOR=$ZVM_CURSOR_BEAM
ZVM_VISUAL_MODE_CURSOR=$ZVM_CURSOR_BLOCK
ZVM_VISUAL_LINE_MODE_CURSOR=$ZVM_CURSOR_BLOCK
ZVM_OPPEND_MODE_CURSOR=$ZVM_CURSOR_UNDERLINE
function zvm_after_select_vi_mode() {
case $ZVM_MODE in
$ZVM_MODE_NORMAL)
;;
$ZVM_MODE_INSERT)
;;
$ZVM_MODE_VISUAL)
;;
esac
}
ZVM_CURSOR_STYLE_ENABLED=false
Key Bindings Reference
Mode Switching
| Key | Action |
|---|
ESC or Ctrl-[ | Enter Normal mode |
i | Insert before cursor |
a | Append after cursor |
I | Insert at line start |
A | Append at line end |
v | Enter Visual mode |
V | Enter Visual Line mode |
Navigation (Normal Mode)
| Key | Action |
|---|
h/l | Left/right |
j/k | Down/up in history |
w/W | Forward word |
b/B | Backward word |
e/E | End of word |
0 | Start of line |
^ | First non-blank |
$ | End of line |
f{char} | Find char forward |
F{char} | Find char backward |
t{char} | Till char forward |
T{char} | Till char backward |
Editing (Normal Mode)
| Key | Action |
|---|
x | Delete char |
dd | Delete line |
D | Delete to end |
cc | Change line |
C | Change to end |
yy | Yank line |
p/P | Paste after/before |
u | Undo |
Ctrl-r | Redo |
Text Objects
| Key | Action |
|---|
ciw | Change inner word |
daw | Delete a word (with space) |
ci" | Change inside quotes |
da( | Delete around parens |
vi[ | Select inside brackets |
KEYTIMEOUT Considerations
The KEYTIMEOUT variable affects multi-key sequences:
export KEYTIMEOUT=10
Workarounds for escape delay:
bindkey -M viins 'jk' vi-cmd-mode
bindkey -M viins 'jj' vi-cmd-mode
Powerlevel10k
Installation
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git \
${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k
ZSH_THEME="powerlevel10k/powerlevel10k"
p10k configure
Instant Prompt Setup
Add at the very top of ~/.zshrc (before anything else):
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi
Add at the end of ~/.zshrc:
[[ -f ~/.p10k.zsh ]] && source ~/.p10k.zsh
Configuration Options
Key settings in ~/.p10k.zsh:
typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(
os_icon
dir
vcs
newline
prompt_char
)
typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(
status
command_execution_time
background_jobs
virtualenv
kubecontext
azure
aws
vi_mode
context
time
)
typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=always
typeset -g POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_unique
typeset -g POWERLEVEL9K_SHORTEN_DIR_LENGTH=3
typeset -g POWERLEVEL9K_VI_INSERT_MODE_STRING=''
typeset -g POWERLEVEL9K_VI_COMMAND_MODE_STRING='NORMAL'
typeset -g POWERLEVEL9K_VI_MODE_NORMAL_FOREGROUND=0
typeset -g POWERLEVEL9K_VI_MODE_NORMAL_BACKGROUND=2
Performance Tuning
typeset -g POWERLEVEL9K_DISABLE_GITSTATUS=false
typeset -g POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY=1000
typeset -g POWERLEVEL9K_VCS_BACKENDS=(git)
typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(status command_execution_time vi_mode)
Performance Summary
Benchmark Results (zsh-bench)
| Metric | Target | Powerlevel10k |
|---|
| First prompt lag | <50ms | 24ms |
| Command lag | <10ms | 15ms |
| Git status (small) | <30ms | <10ms |
| Git status (large) | <100ms | Async/instant |
Architecture
Powerlevel10k (gitstatus daemon):
┌─────────────┐ pipes ┌─────────────┐
│ Zsh │ <============> │ gitstatusd │
│ (prompt) │ │ (C++ daemon)
└─────────────┘ └─────────────┘
│ │
│ async │ keeps state
│ never blocks │ in memory
▼ ▼
Instant prompt Fast git queries
Benchmarking Your Setup
Using zsh-bench
git clone https://github.com/romkatv/zsh-bench ~/zsh-bench
~/zsh-bench/zsh-bench
Manual Timing
time zsh -i -c exit
TIMEFMT='%*E seconds'
time (for i in {1..10}; do zsh -i -c 'print -P "$PROMPT"' >/dev/null; done)
Troubleshooting
Slow Prompt
zsh -xv
P10k: gitstatus Failed
rm -rf ~/.cache/gitstatus
exec zsh
Vi Mode Not Working
bindkey -l | grep vi
echo $KEYMAP
bindkey -v
Cursor Not Changing
- Verify terminal supports cursor escape codes
- Check
zle-keymap-select is defined: whence -f zle-keymap-select
- Some terminals (like Apple Terminal) have limited cursor support
- Try iTerm2 or Alacritty for full support
References
External Links
Gotchas
- P10k instant prompt MUST be at the very top of
.zshrc, before any output: Even an echo or unguarded command -v produces a warning and disables instant prompt. Move all stdout-producing code below the instant prompt block or wrap with &>/dev/null.
KEYTIMEOUT=1 breaks multi-key bindings like jk to escape: Below 10 (100ms), zsh can't distinguish j then k from a multi-byte ESC sequence. Symptom: prompt accepts j and k as literal characters in normal mode.
bindkey -v resets ALL previous bindings: Custom bindings made before bindkey -v are silently wiped. Place vi mode setup first, then all custom bindkey calls.
- Cursor escape codes (
\e[6 q) don't work in tmux without passthrough: tmux strips DECSCUSR by default — add set -ga terminal-overrides ',*:Ss=\E[%p1%d q:Se=\E[ q' to .tmux.conf or cursor stays as block.
- gitstatusd caches per-repo: a
git reset --hard from outside zsh shows stale prompt: P10k's daemon hasn't seen the change. Wait ~2s, run git status manually, or p10k reload to force refresh.
- Plugin load order: zsh-vim-mode AFTER autosuggestions and syntax-highlighting: Reversed order causes vim-mode to overwrite zle widgets — symptoms range from broken cursor changes to nonfunctional ESC. Verify with
zle -lL | grep widget-name.