Skip to content

Latest commit

 

History

History
697 lines (535 loc) · 25.9 KB

tips.md

File metadata and controls

697 lines (535 loc) · 25.9 KB

Advanced configuration tips

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.

tmux

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'

Prompt at bottom

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.

Autosuggestions

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

Shell integration

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

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

Terminal title

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.

SSH

SSH teleportation

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

Extra dotfiles

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.

Better hostname reporting

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.

SSH config

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.

Persistent and shared command history

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.

Unattended teleportation

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.

Current directory

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.

Completions

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

fzf

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.

Word-based widgets

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.

Oh My Zsh

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.

Backup and restore

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

vi mode

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.

  1. Select emacs when asked by the installer about your preferred keymap.
  2. Add bindkey -v below z4h init in ~/.zshrc.
  3. Add your own bindings with bindkey or z4h bindkey below bindkey -v.

Managing dotfiles

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:

When I press Ctrl+P once, I get public showing up in prompt and git status in prompt corresponds to dotfiles-public repo. All git 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 type git add baz, git commit, etc. If I hit Ctrl+P another time, it activates dotfiles-private. Another Ctrl+P gets me to normal state.

Alternative ZDOTDIR

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.

Privileged shell

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.

Homebrew

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.