blog/content/posts/zsh-zim-setup.md
2023-08-06 19:20:37 -07:00

270 lines
10 KiB
Markdown

+++
date = "2023-08-06"
draft = false
path = "/blog/zsh-zim-setup"
tags = ["zim", "zsh", "workflow"]
title = "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.
[prezto]: https://github.com/sorin-ionescu/prezto
## What's broken anyway?
It's possible to profile zsh execution [using `zprof`][zprof].
[zprof]: https://docs.jade.fyi/zsh/zsh.html#The-zsh_002fzprof-Module
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.
[zim]: https://github.com/zimfw/zimfw
## 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:
```bash
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`:
```bash
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].
[Completion System Configuration]: https://docs.jade.fyi/zsh/zsh.html#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 at `f._`, 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`][complete-help] by pressing `C-x h`:
[complete-help]: https://docs.jade.fyi/zsh/zsh.html#index-_005fcomplete_005fhelp-_0028_005eXh_0029
```
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):
```bash
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.
[atuin]: https://github.com/atuinsh/atuin
## 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.
[zsh-syntax-highlighting]: https://github.com/zsh-users/zsh-syntax-highlighting
You can also get the fish-like history suggestions with [zsh-autosuggestions].
[zsh-autosuggestions]: https://github.com/zsh-users/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`][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 of `BufDelete`: if you have `'hidden'` set,
`nvr` will not return since it listens for `BufDelete` (which will not be
fired unless `:bde` is explicitly called, or `nvr` is invoked as `nvr
--remote-wait +'setlocal bufhidden=delete' file`).
- Handle weird file names better: `nvr` can't open files that look like
options.
[nvimsplit]: https://github.com/lf-/dotfiles/tree/main/programs/nvimsplit
[nvim-remote]: https://github.com/mhinz/neovim-remote
## 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.
[telescope.nvim]: https://github.com/nvim-telescope/telescope.nvim
## 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.
[nushell]: https://www.nushell.sh/