Setting up zim with zsh
I have been using an unmaintained fork of prezto since 6 years ago, which has
been mostly completely fine. However, one day (and possibly due to my own
actions), it started having severe startup performance issues on my NixOS
system one day, and enough was enough: I will finally figure out how zsh works.
It would be unfair to blame prezto
for the startup performance issues, since
the version in question is so old.
What's broken anyway?
It's possible to profile zsh execution using zprof
.
To do this, put zmodload zsh/zprof
into the top of your .zshrc
, and then
zprof
at the end of it.
This will print out a listing of where the time went:
num calls time self name
-----------------------------------------------------------------------------------
1) 2 36.07 18.04 58.27% 10.75 5.37 17.36% pmodload
2) 2 9.71 4.86 15.69% 9.71 4.86 15.69% compaudit
3) 2 5.52 2.76 8.92% 5.52 2.76 8.92% zle-reset-prompt
4) 5 5.01 1.00 8.09% 4.06 0.81 6.55% _zsh_highlight_main_highlighter
<snip>
This output is from another machine not as bad as my laptop; I found that the
time went into pmodload
in prezto, which is a function that sources modules;
not terribly interesting.
Undoubtedly, it would have been possible to fix the ancient prezto fork, but it needed rewriting anyway.
Rewrite it
Since I'm rewriting it anyway, I switched to zim, a fancy zsh module manager thingy. It's distinctly possible that I might wind up canning zim in the future due to opinions about software pinning, but at least it's much much smaller and easier to delete.
In particular, zim itself is merely a downloader, and is not even run on normal shell startup if nothing has changed, which means that I can throw away their code any day.
Values
I don't want to have dependencies ever update unexpectedly, and I want my environment to be exactly the same on different machines. Automatic updating for such a critical tool as my shell configuration, which does not break itself and for the most part never actually needs updates is highly undesirable.
zim
does not really agree with this view, but it can be made to work with it
well enough.
What zim does
zim
does two things: it downloads dependencies for you, and it compiles a
static init.zsh
file that is loaded on shell startup. The dependency
downloading is done via either GitHub tarballs or git
, and is invoked by
zimfw install
(and zimfw init
).
The installation process skips directories that already exist (!).
zim
compiles a static file init.zsh
, which is sourced on shell startup. It
looks something like the following:
zimfw() { source /home/jade/.local/share/zim/zimfw.zsh "${@}" }
zmodule() { source /home/jade/.local/share/zim/zimfw.zsh "${@}" }
fpath=(/home/jade/.local/share/zim/modules/prompt-pwd/functions /home/jade/.local/sh
are/zim/modules/git-info/functions /home/jade/.local/share/zim/modules/utility/funct
ions /home/jade/.local/share/zim/modules/zsh-completions/src ${fpath})
autoload -Uz -- prompt-pwd coalesce git-action git-info mkcd mkpw
source /home/jade/.local/share/zim/modules/utility/init.zsh
source /home/jade/.local/share/zim/modules/input/init.zsh
source /home/jade/.dotfiles/configs/zsh/prompt.zsh-theme
# <snip>
Making zim work
The dependency management strategy I used for zim is git subtree
vendoring.
The reason I am doing it this way is that it ensures that zim is the exact
selected version and avoids submodules.
Since zimfw install
ignores already-existing directories, we can simply
check in the entire modules/
directory inside ZIM_HOME
after running zimfw install
and deleting modules/
from the gitignore. This works much better if you
use degit
, the GitHub-tarball downloader, since otherwise you would have
nested git repos.
zim
also chooses to attempt to update itself monthly via the Internet, so
that needs to go too.
This leads to the following zim configuration in .zshrc
, prior to sourcing
${ZIM_HOME}/init.zsh
:
zstyle ':zim:zmodule' use 'degit'
zstyle ':zim' 'disable-version-check' 'true'
Then just run zimfw install
and check in the modules. Now it's pinned, and
zimfw update
will update the pins.
Completion
zsh
has a very advanced completion system, which I have years-old muscle
memory with, so I am going to be tweaking my config until it feels right again.
Settings for completions are under zstyle ':completion:func:completer:command:argument:tag' 'somestyle'
. Not all of
these fields have to be used; globs can be used. For more details, see
Completion System Configuration.
For example, the following will set the completer
setting for all contexts
to a configuration which does the following:
_extensions
: if the cursor is atf._
, it will try to complete a file extension._complete
: invoke the standard completion system._match
: expands globs into the applicable completions when tab is pressed ^([citation needed])._approximate
: tries to autocorrect things to a similar completion; fixes paths and other misspelled things.
zstyle ':completion:*' completer _extensions _complete _match _approximate
To find out where the completion system is getting ideas from, invoke
_complete_help
by pressing C-x h
:
dev/zlog » ls new*
tags in context :completion::approximate:::
corrections original (_approximate)
tags in context :completion::approximate-1:ls::
argument-rest options (_arguments _ls)
History
By default zsh does not save history! Also, there are various options that probably should be changed:
# save a lot of history
HISTSIZE=1000000
SAVEHIST=1000000
# all instances share the same history
setopt SHARE_HISTORY
# history expansion goes into the editor buffer first
setopt HIST_VERIFY
# don't show dupes in history search
setopt HIST_FIND_NO_DUPS
# don't history commands beginning in space (consistent with bash)
setopt HIST_IGNORE_SPACE
# allow comments in the shell
setopt INTERACTIVE_COMMENTS
History search is built into the shell, but it is not fully bound by default,
which is kind of odd. The following will bind, effectively, the emacs
bindings in viins
mode such that you can C-r
and C-s
as in bash (I
know about the conflict with C-r
; I don't use undo in my shell):
bindkey -M vicmd "?" history-incremental-pattern-search-backward
bindkey -M vicmd "/" history-incremental-pattern-search-forward
bindkey -M viins '\C-R' history-incremental-pattern-search-backward
bindkey -M viins '\C-S' history-incremental-pattern-search-forward
unsetopt FLOW_CONTROL # disable C-s/C-q in the editor
When inside a search already, this is considered viins
mode, so hitting C-r
will cycle through the previous results (it took me 6 years to fix being able
to get more than one result in history).
You can also search history with history 1 | grep
.
In the future I am probably going to adopt atuin for nicer shell history with better metadata and self-hosted syncing across machines.
Syntax highlighting and fancy autosuggestions
Alternate title: Something fish
y going on here!
Use zmodule zsh-users/zsh-syntax-highlighting
to get
zsh-syntax-highlighting, which adds highlighting in the prompt. I find this
super valuable since it highlights file names and makes it obvious before even
running a command whether the path is right.
You can also get the fish-like history suggestions with zsh-autosuggestions.
Bonus: nvim integration
Fun fact: nvim exposes a socket at $NVIM
to subprocesses, so if you use nvim
as a terminal emulator, you can send arbitrary RPC to the editor from the shell
session.
This is great: set $VISUAL = 'nvr --remote-wait'
in your nvim configuration
to use nvim-remote
to open files in the surrounding nvim
instance. This makes git commit
, sudo -e
, and other things magically just
work.
I rewrote nvim-remote
in Rust as nvimsplit, which is what I use; the
differences are as follows:
- Use the
BufHidden
event instead ofBufDelete
: if you have'hidden'
set,nvr
will not return since it listens forBufDelete
(which will not be fired unless:bde
is explicitly called, ornvr
is invoked asnvr --remote-wait +'setlocal bufhidden=delete' file
). - Handle weird file names better:
nvr
can't open files that look like options.
Future improvements
I want to have a fuzzy file search similar to telescope.nvim in my shell. This is probably the most useful one improvement I could make. One day.
Finale
My shell works again :D
For me the shell is very much a tool that must work 100% of the time and not something to mess with often, as may be evidenced by it having been last seriously messed with 6 years ago. The rewrite project has been a success in this respect: I definitely have less code around, and it is now more maintainable.
It would be kind of nice to switch to PowerShell or nu shell on my Linux
machines, but there's enough broken programs that assume POSIXness from
$SHELL
, and shell replacement is not something I really want to do.