The default configuration in Zsh for Humans is intentionally conservative. It's meant to be non-surprising for new users and robust. Experienced Zsh users are encouraged to customize their config to unlock the full potential of their shell.
-
- 15.1. Alternative
ZDOTDIR
- 15.1. Alternative
If you choose No when asked by the installer whether zsh
should always run
in tmux
, you'll have the following snippet in ~/.zshrc
:
# Don't start tmux.
zstyle ':z4h:' start-tmux no
Several features in Zsh for Humans require knowing the content of the terminal
screen, and with the above option this condition won't be satisfied. If you
remove this zstyle
line, Zsh for Humans will automatically start a
stripped-down version of tmux
(referred to as "integrated tmux" in the source
code and discussions) that should enable the extra features with no other
visible effects. This used to be the default in Zsh for Humans for a long time
but eventually it's been changed because there are corner cases where integrated
tmux can cause issues. Try removing this line and see if everything still works.
If your terminal has a feature that allows it to open a new tab or window in the same directory as the current tab, and it doesn't work, add the following option:
zstyle ':z4h:' propagate-cwd yes
If terminal title breaks, see Terminal Title.
If vertically resizing the terminal window breaks scrollback, add this option:
zstyle ':z4h:' term-vresize top
If mouse wheel scrolling stops working in some applications, enable mouse support for them explicitly. For example:
alias nano='nano --mouse'
Having prompt always in the same location allows you to find it quicker and to position your terminal window so that looking at prompt is most comfortable.
Add the following option to ~/.zshrc
to place prompt at the bottom when Zsh
starts and upon pressing Ctrl+L:
# Move prompt to the bottom when zsh starts and on Ctrl+L.
zstyle ':z4h:' prompt-at-bottom 'yes'
This feature requires that start-tmux
is not set to no
.
If you have a habit of running clear
instead of pressing Ctrl+L,
you can add this alias:
alias clear=z4h-clear-screen-soft-bottom
Note that having prompt always at the top is impossible.
Most key shortcuts that move the cursor behave consistently in the presence of
autosuggestions. The only exceptions are forward-char
, vi-forward-char
and
end-of-line
. These widgets accept the full autosuggestion instead of just one
character or one line. This can be fixed with the following options:
zstyle ':z4h:autosuggestions' forward-char partial-accept
zstyle ':z4h:autosuggestions' end-of-line partial-accept
Add the following option to ~/.zshrc
:
# Mark up shell's output with semantic information.
zstyle ':z4h:' term-shell-integration 'yes'
This enables extra features in terminals that understand OSC 133 (iTerm2, kitty, and perhaps others). It also fixes horrific mess when resizing terminal window, provided that you've enabled integrated tmux.
In iTerm2 you'll see blue triangles to the left of every prompt. This can be disabled in iTerm2 preferences.
Prompt can be configured with p10k configure
. Some options work very well
together: try two-line prompt, sparse (adds an empty line before prompt), and
transient prompt. If you are optimizing for productivity, use Lean style and
choose Few icons rather than Many. The extra icons from Many are
decorative. See: What is the best prompt style in the configuration wizard.
Add the following option to ~/.zshrc
to make transient prompt work
consistently when closing an SSH connection:
z4h bindkey z4h-eof Ctrl+D
setopt ignore_eof
This preserves the default zsh behavior on Ctrl+D. You can bind z4h-exit
instead of z4h-eof
if you want Ctrl+D to always exit the shell.
If you are using a two-line prompt with an empty line before it, add this for smoother rendering:
POSTEDIT=$'\n\n\e[2A'
If you are using a one-line prompt with an empty line, or a two-line prompt without an empty line, add this instead:
POSTEDIT=$'\n\e[A'
You can bind Enter
to z4h-accept-line
to insert a newline instead of
displaying the secondary prompt (a.k.a. PS2
) when the currently typed
command is incomplete.
z4h bindkey z4h-accept-line Enter
Some terminals by default do not allow shell to set tab and window title. This can be changed in the terminal preferences.
Terminal title can be customized with :z4h:term-title
style. Here are the
defaults:
zstyle ':z4h:term-title:ssh' preexec '%n@%m: ${1//\%/%%}'
zstyle ':z4h:term-title:ssh' precmd '%n@%m: %~'
zstyle ':z4h:term-title:local' preexec '${1//\%/%%}'
zstyle ':z4h:term-title:local' precmd '%~'
:z4h:term-title:ssh
is applied when connected over SSH while
:z4h:term-title:local
is applied to local shells.
preexec
title is set before executing a command: $1
is the unexpanded
command line, $2
is the same command line after alias expansion.
precmd
title is set after executing a command. There are no positional
arguments.
All values undergo prompt expansion.
Tip: Add %*
to preexec
to display the time when the command started
executing.
Tip: Replace %m
with ${${${Z4H_SSH##*:}//\%/%%}:-%m}
. This makes a
difference when using SSH teleportation: the title will show the
hostname as you typed it on the command line when connecting rather than
the hostname reported by the remote machine.
When you connect to a remote host over SSH, your local Zsh for Humans
environment can be teleported over to it. The first login to a remote host may
take some time. After that it's as fast as normal ssh
.
SSH teleportation can be enable per host. By default it's disabled for all hosts. You can use either a blacklist approach:
# Enable SSH teleportation by default.
zstyle ':z4h:ssh:*' enable yes
# Disable SSH teleportation for specific hosts.
zstyle ':z4h:ssh:example-hostname1' enable no
zstyle ':z4h:ssh:*.example-hostname2' enable no
Or a whitelist approach:
# Disable SSH teleportation by default.
zstyle ':z4h:ssh:*' enable no
# Enable SSH teleportation for specific hosts.
zstyle ':z4h:ssh:example-hostname1' enable yes
zstyle ':z4h:ssh:*.example-hostname2' enable yes
If your shell environment requires extra files other than zsh rc files (which
are teleported by default), add them to send-extra-files
:
zstyle ':z4h:ssh:*' send-extra-files '~/.nanorc' '~/.env.zsh'
You can add directories here as well. Don't add anything heavy as it'll slow down SSH connection.
NOTE: Remote files and directories get silently overwritten when teleporting.
NOTE: If a file doesn't exist locally, it'll be silently deleted on the remote host when teleporting.
When connected over SSH, by default prompt and terminal title will display the
hostname as reported by the remote machine. Sometimes it's not the same as
what you've passed to ssh
on the command line and usually you would want to
see the latter. To achieve this, use ${${${Z4H_SSH##*:}//\%/%%}:-%m}
instead
of %m
in configuration options. For example, here's how you can configure
terminal title:
zstyle ':z4h:term-title:ssh' preexec '%n@'${${${Z4H_SSH##*:}//\%/%%}:-%m}': ${1//\%/%%}'
zstyle ':z4h:term-title:ssh' precmd '%n@'${${${Z4H_SSH##*:}//\%/%%}:-%m}': %~'
And here's prompt:
typeset -g POWERLEVEL9K_CONTEXT_TEMPLATE=%n@${${${Z4H_SSH##*:}//\%/%%}:-%m}
The latter should go in ~/.p10k.zsh
. You might already have some CONTEXT
templates in there. Customize them as needed.
For better user experience with SSH add the following stanza to ~/.ssh/config
:
Host *
ServerAliveInterval 60
ConnectTimeout 10
AddKeysToAgent yes
EscapeChar `
ControlMaster auto
ControlPersist 72000
ControlPath ~/.ssh/s/%C
See man ssh_config
for the meaning of these options and adjust them
accordingly.
Make sure that ~/.ssh/s
is an existing directory with 0700
mode.
The above config remaps EscapeChar
from the default tilde to backtick because
you often start zsh commands with tilde (and it's annoying that nothing shows
up) but you never start commands with backtick.
If your OS doesn't start SSH agent automatically, add this to ~/.zshrc
:
zstyle ':z4h:ssh-agent:' start yes
zstyle ':z4h:ssh-agent:' extra-args -t 20h
It's a good idea to list all hosts that you SSH to in ~/.ssh/config
. Like
this:
Host pihole
HostName 192.168.1.42
User pi
Host blog
HostName 10.100.1.2
User admin
If you do this, you can configure ssh
and similar commands to
complete hostnames nicely.
Zsh for Humans can pull command history from remote hosts when you close an SSH connection. It can also send command history to the remote host when connecting. This allows you to retain command history from remote hosts even when they get wiped. It also allows you to share command history between hosts. The mechanism is very flexible but not easy to configure. Here's something to get you started.
# This function is invoked by zsh4humans on every ssh command after
# the instructions from ssh-related zstyles have been applied. It allows
# us to configure ssh teleportation in ways that cannot be done with
# zstyles.
#
# Within this function we have readonly access to the following parameters:
#
# - z4h_ssh_client local hostname
# - z4h_ssh_host remote hostname as it was specified on the command line
#
# We also have read & write access to these:
#
# - z4h_ssh_enable 1 to use ssh teleportation, 0 for plain ssh
# - z4h_ssh_send_files list of files to send to the remote; keys are local
# file names, values are remote file names
# - z4h_ssh_retrieve_files the same as z4h_ssh_send_files but for pulling
# files from remote to local
# - z4h_retrieve_history list of local files into which remote $HISTFILE
# should be merged at the end of the connection
# - z4h_ssh_command command to use instead of `ssh`
function z4h-ssh-configure() {
emulate -L zsh
# Bail out if ssh teleportation is disabled. We could also
# override this parameter here if we wanted to.
(( z4h_ssh_enable )) || return 0
# Figure out what kind of machine we are about to connect to.
local machine_tag
case $z4h_ssh_host in
ec2-*) machine_tag=ec2;;
*) machine_tag=$z4h_ssh_host;;
esac
# This is where we are locally keeping command history
# retrieved from machines of this kind.
local local_hist=$ZDOTDIR/.zsh/history/retrieved_from_$machine_tag
# This is where our $local_hist ends up on the remote machine when
# we connect to it. Command history from files with names like this
# is explicitly loaded by our zshrc (see below). All new commands
# on the remote machine will still be written to the regular $HISTFILE.
local remote_hist='"$ZDOTDIR"/.zsh/history/received_from_'${(q)z4h_ssh_client}
# At the start of the SSH connection, send $local_hist over and
# store it as $remote_hist.
z4h_ssh_send_files[$local_hist]=$remote_hist
# At the end of the SSH connection, retrieve $HISTFILE from the
# remote machine and merge it with $local_hist.
z4h_retrieve_history+=($local_hist)
}
# Load command history that was sent to this machine over ssh.
() {
emulate -L zsh -o extended_glob
local hist
for hist in $ZDOTDIR/.zsh/history/received_from_*(NOm); do
fc -RI $hist
done
}
You'll need to add this block to ~/.zshrc
below z4h init
. Before trying it
out you'll probably want to modify the logic that computes machine_tag
based
on $z4h_ssh_host
although you can also use it as is -- there is a reasonable
fallback.
If you are defining z4h-ssh-configure
, you don't actually need to use
ssh-specific zstyles but you still can if you want to. The function is invoked
after zstyles are applied, so you can observe and/or override their effect
within z4h-ssh-configure
. For example, z4h_ssh_enable
within the function is
set to 0 or 1 according to the value of zstyle :z4h:ssh:$hostname enable
. The
implementation of z4h-ssh-configure
posted above bails out if z4h_ssh_enable
is zero, so it doesn't do anything unless you enable SSH teleportation via
zstyle
for the target host. You could instead set z4h_ssh_enable
in the
function itself based on $z4h_ssh_host
or anything else.
You can add the following line at the top of z4h-ssh-configure
to see the
initial values of all ssh parameters that Zsh for Humans lets you read/write.
typeset -pm 'z4h_ssh_*'
You'll notice that there are a few more parameters than what is documented in
the comments above z4h-ssh-configure
. Those are low-level blocks of code that
get executed on the remote host. You probably shouldn't touch them.
You can teleport Zsh for Humans to a remote host with a script like this:
#!/usr/bin/env -S zsh -i
emulate -L zsh -o no_ignore_eof
ssh -t hostname <<<exit
Replace hostname
with a real hostname.
The shebang says to execute this script with zsh -i
, which makes z4h
function available to it.
After you run this script, it's guaranteed that SSH teleportation will be fast and won't perform the installation or update.
To forcefully update Zsh for Humans on the remote machine, replace the last line with this:
ssh -t hostname <<<$'z4h update\nexit'
Usually this shouldn't be necessary because SSH teleportation automatically
updates Zsh for Humans on the remote host if your local rc files require a newer
version than what's available there. When a new feature is added to Zsh for
Humans (a function, an alias, a zstyle, etc.), version gets bumped. When
teleporting, the version number of the local Zsh for Humans installation is sent
over to the remote (it's the first part of $Z4H_SSH
) and the remote is updated
if its version is lower. This ensures that your rc files are compatible with
Zsh for Humans on the remote host.
Zsh for Humans stores persistent directory history. It gets loaded into the
builtin dirstack
when you start zsh. Try opening a new terminal and typing
cd -<TAB>
-- you'll see the history. You can also hit Alt+Left
(Shift+Left on macOS) to go back in dirstack
. This is useful if you
want to create a new terminal tab and cd
into the last directory you've
visited, or to go back after a cd
. Alt+Left/Alt+Right
(Shift+Left/Shift+Right on macOS) work like Back/Forward
buttons in a web browser.
Alt+Up (Shift+Up on macOS) goes to the parent directory and Alt+Down (Shift+Down on macOS) goes to a subdirectory. Since there are many subdirectories, the latter asks you to choose.
There is also Alt+R for fzf over directory history. This is the closest thing to autojump, z and similar tools.
You might want to configure things a bit differently:
zstyle ':z4h:fzf-dir-history' fzf-bindings tab:repeat
zstyle ':z4h:cd-down' fzf-bindings tab:repeat
z4h bindkey z4h-fzf-dir-history Alt+Down
This rebinds Alt+Down to z4h-fzf-dir-history
-- the widget that you
can invoke via Alt+R by default. You'll no longer have a binding for
z4h-cd-down
but that's OK because you can get the same behavior with
Alt+Down Tab.
The two zstyle
lines rebind Tab in two fzf-based widgets from
the default up
to repeat
. The latter causes the selection to get accepted
(like pressing Enter) and immediately opens fzf once again. When you
invoke z4h-fzf-dir-history
, the first entry is always the current directory,
so repeat
on that will repopulate fzf with subdirectories of the current
directory -- just like z4h-cd-down
. You can press Tab on other
entries, too, if you need to go into their subdirectories.
Enable recursive file completions:
# Recursively traverse directories when TAB-completing files.
zstyle ':z4h:fzf-complete' recurse-dirs yes
This takes a bit of getting used to but once you do, it's a massive time saver.
Rebind Tab in fzf from up
to repeat
:
zstyle ':z4h:fzf-complete' fzf-bindings tab:repeat
Now Tab in fzf will accept the selection (like pressing Enter) and immediately open fzf once again if the current word isn't fully specified yet. It's very useful when TAB-completing file arguments. Instead of waiting in fzf for all files and directories to be traversed (assuming you've enabled recursive file completions), you can accept a directory with Tab to narrow down the search.
You can undo and redo completions the same way as any other command line
changes. You can find their bindings in your ~/.zshrc
and might want to rebind
them to something else.
Tip: Use Tab to expand and verify globs and undo the expansion before
executing the command. For example, you can type rm **/*.orig
, press
Tab to expand the glob, check that it looks good, press
Ctrl+/ to undo the expansion and execute the command. (It's a good
idea execute commands with glob arguments in order to have them this way in
history. This allows you to re-execute them even when the set of **/*.orig
files changes.)
Try flipping setup no_auto_menu
to setopt auto_menu
and see if you like it.
This will automatically press Tab for the second time when the first
Tab inserts an unambiguous prefix.
If all hosts you SSH to are listed in ~/.ssh/config
(good idea), add this to
improve completions for ssh
and similar commands:
zstyle ':completion:*:ssh:argument-1:' tag-order hosts users
zstyle ':completion:*:scp:argument-rest:' tag-order hosts files users
zstyle ':completion:*:(ssh|scp|rdp):*:hosts' hosts
Familiarize yourself with fzf query syntax.
The highlight color can be changed (from the default poisonous pink) with the following option:
zstyle ':z4h:*' fzf-flags --color=hl:5,hl+:5
Replace 5
with the color of your choice. Here's a handy one-liner to print
the color table:
for i in {0..255}; do print -Pn "%K{$i} %k%F{$i}${(l:3::0:)i}%f " ${${(M)$((i%6)):#3}:+$'\n'}; done #colors
That #colors
at the end is technically a comment but you can use it as a tag.
Next time you need to find this command, press Ctrl+R and type
#colors
. Tagging commands in this way is a good habit.
You can bind *-zword
widgets that operate on whole shell arguments. For
example, ls '/foo/bar baz'
has two zwords: ls
and '/foo/bar baz'
. These
widgets are z4h-forward-zword
, z4h-backward-zword
, z4h-kill-zword
and
z4h-backward-kill-zword
. There are word
variants of all these widgets, too.
They behave the same as word-based navigation in Visual Studio Code.
The default ~/.zshrc
has several references to ohmyzsh
. They don't do
anything useful. Their only purpose is to show how you can load third-party
plugins. If you don't intend to load plugins from Oh My Zsh, remove all lines
with ohmyzsh
in them from ~/.zshrc
. This will speed up bootstrapping of Zsh
for Humans when SSH teleporting to a host for the first time.
If you want to load plugins from Oh My Zsh, check what you get from them. The
vast majority of Oh My Zsh plugins don't do anything useful on top of Zsh for
Humans. If you are loading a plugin for the aliases it provides, it's almost
always a better idea to copy the specific aliases to your ~/.zshrc
instead of
loading the plugin.
It's highly recommended to store your dotfiles in a git repository. As far as Zsh for Humans goes, you'll need to store these files:
~/.zshenv
~/.zshrc
~/.p10k*.zsh
(there can be more than one).
You don't need to run Zsh for Humans installer on a new machine. Simply copy/restore these files and Zsh for Humans will bootstrap itself. If you don't have zsh on the machine, you can bootstrap Zsh for Humans from any Bourne-based shell with the following command:
Z4H_BOOTSTRAPPING=1 . ~/.zshenv
The installer refuses to do anything if you select vi when asked about your preferred keymap. If you don't mind manually defining a few bindings, you can use Zsh for Humans in vi mode.
- Select emacs when asked by the installer about your preferred keymap.
- Add
bindkey -v
belowz4h init
in~/.zshrc
. - Add your own bindings with
bindkey
orz4h bindkey
belowbindkey -v
.
It's highly recommended to store your dotfiles in a git repository. This allows you to restore your shell environment when your development machine dies. It also lets you synchronize dotfiles across different development machines. If you aren't using SSH teleportation, you can also use git to pull dotfiles onto remote hosts. With SSH teleportation this is automatic.
There are many tools out there that help you with dotfiles management. Choose what you like. As an option, here's what the author of Zsh for Humans uses.
I have two git repos where I store my stuff: dotfiles-public and dotfiles-private. Both are overlaid over
$HOME
(that is, their worktree is$HOME
), so I can version any file without moving or symlinking it. I sync dotfiles between my dev machines (a desktop and two laptops) with sync-dotfiles, which I run manually. This function synchronizes both repos.I store command history in dotfiles-private. There is a separate file per combination of local and remote machine (there is no remote machine for commands executed locally). The point of this separation is twofold. The first reason is that it gives local history priority: when I hit Ctrl+R on machine A, commands that I ran on machine A are displayed before the commands from other machines (assuming I'm sharing history from other machines with A). The second reason is that it avoids merge conflicts because every history file is modified only on one machine.
There are a few more important bits to my dotfiles management:
- my_git_repo prompt segment.
- toggle-dotfiles zle widget.
- A keybinding for
toggle-dotfiles
.When I press Ctrl+P once, I get
public
showing up in prompt and git status in prompt corresponds to dotfiles-public repo. Allgit
commands also target this repo. So if I'm in~/foo/bar
and want to add./baz
to dotfiles-public, I hit Ctrl+P and typegit add baz
,git commit
, etc. If I hit Ctrl+P another time, it activates dotfiles-private. Another Ctrl+P gets me to normal state.
By default zsh startup files are stored in the home directory. If you want to
store them in ~/.config/zsh
instead, use this script to migrate.
Note that ~/.zshenv
will still exist. Without it zsh won't know where to look
for startup files.
You can open a privileged shell with sudo -Es
. This will start zsh as root
with your regular rc files and $HOME
will point to your regular home
directory.
When referencing files and directories managed by Homebrew,
you can rely on HOMEBREW_PREFIX
being automatically set. This is much faster
than invoking brew --prefix
. For example, here's how you can load
asdf:
z4h source -- ${HOMEBREW_PREFIX:+$HOMEBREW_PREFIX/opt/asdf/libexec/asdf.sh}
This line won't do anything unless asdf
has been installed with brew
.