Write in neovim anywhere and everywhere

If you write in neovim, you want to write in neovim everywhere.

The below script acts as an intermediary solution, by calling up a neovim instance, allowing you to write as you know and love, and by executing :wq, closes the window and pastes the text into the previously focussed text box.

Here is a quick and dirty demo video.

It uses skhd to launch the script via a global keybind, and yabai to tile the window nicely in place. Additionally, jq is used to halt script execution until we are done editing our text.

I took a lot of cues from vim-anywere. But vim-anywhere required macvim, which I didn’t want to have to install and setup.

My solution uses your neovim config as it is (as we’re really just launching a terminal window, and running neovim inside).

This is mac specific as it is. But the only parts that need to be change for Linux is the copy program (look for pbcopy) and the osascript (which is only used for getting the current app before execution, and refocussing on that program after the kitty process finishes).

Requirements

  • skhd (to set a global hotkey)
  • yabai (optional, for window management)
  • jq (for extracting the kitty pid)
  • vim / neovim / editor of choice

Breaking it up

Below reads as: code -> explanation.

#!/usr/bin/env bash

# invoked via global hotkey (skhd).
# refreshes a temp file, opens in neovim, copys to clipboard on quit.
# heavily inspired by vim-anywhere (https://github.com/cknadler/vim-anywhere)

kitty=$(which kitty)
nvim=$(which neovim)
pbcopy=$(which pbcopy)
current_app=$(osascript $HOME/.scripts/apple/current-app.scpt)
compose_file=/tmp/vimit.md
pid_file=/tmp/vimit-pid

kitty_cmd="kitty @ set-window-title 'vimit' \
    && kitten @ ls | jq '.[].tabs[].windows[] | select(.title == \"vimit\") | .pid' > $pid_file \
    && nvim $compose_file"
# ...

The osascript grabs the currently focussed app. Here is the script.

The kitty_cmd is executed by a kitty terminal process that we launch, it:

  • sets a window title (so that we can filter it easily using jq),
  • extracts the pid and writes it to $pid_file
  • launches neovim by opening a temp file $compose_file
# ...
# cleanup on new execution in case old text needs to be retrieved.
rm $compose_file

# execute kitty command
$kitty -1 -T scratch --session - zsh -l -c "${kitty_cmd}"
#...

Delete the old compose file at the start of the script, in case we need to reference it between invocations of the script (this would not be possible if we deleted it at the end of the script).

Launch the kitty process. -T scratch gives the kitty window a title (“scratch”) that yabai can read (it doesn’t read the “vimit” title that we set in the kitty_cmd. I’m not totally sure why). This allows us to target the launched window with a yabai rule.

# ...
# allow time for pid file to be created and
# wait for kitty process to finish
sleep 1
vimit_pid=$(cat $pid_file)
lsof -p $vimit_pid +r 1 &>/dev/null

# only copy and paste if we actually wrote something
if [[ -e $compose_file ]]; then
    $(which pbcopy) <$compose_file
    osascript -e "activate application \"$current_app\""
    skhd -k "cmd - v" # get skhd to simulate `cmd + v`. faster than osascript
    rm $kitty_ls_file
fi

rm $pid_file

Extract the pid from the file we wrote to and wait for the process to finish with lsof. This will halt execution of the script while the kitty process is running, meaning that we only copy text from the temporary file after it’s been written to (after we execute :wq, which subsequently closes the kitty window and kills the kitty process).

Yabai and Skhd

The yabai rule then looks like this:

# place the window bottom center.
yabai -m rule --add app="(^WezTerm$|^kitty$)" title="^scratch$" opacity=1.0 sticky=on manage=off grid=3:3:1:2:1:1

And for skhd:

alt + cmd - i : ~/.scripts/utility/vimit.sh

Full Script

#!/usr/bin/env bash

# invoked via global hotkey (skhd).
# refreshes a temp file, opens in neovim, copys to clipboard on quit.
# heavily inspired by vim-anywhere (https://github.com/cknadler/vim-anywhere)

kitty=$(which kitty)
nvim=$(which neovim)
pbcopy=$(which pbcopy)
current_app=$(osascript $HOME/.scripts/apple/current-app.scpt)
compose_file=/tmp/vimit.md
pid_file=/tmp/vimit-pid

kitty_cmd="kitty @ set-window-title 'vimit' \
    && kitten @ ls | jq '.[].tabs[].windows[] | select(.title == \"vimit\") | .pid' > $pid_file \
    && nvim $compose_file"

# cleanup on new execution incase old text needs to be retrieved.
rm $compose_file

# execute kitty command
$kitty -1 -T scratch --session - zsh -l -c "${kitty_cmd}"

# allow time for pid file to be created and
# wait for kitty process to finish
sleep 1
vimit_pid=$(cat $pid_file)
lsof -p $vimit_pid +r 1 &>/dev/null

# only copy and paste if we actually wrote something
if [[ -e $compose_file ]]; then
    $(which pbcopy) <$compose_file
    osascript -e "activate application \"$current_app\""
    skhd -k "cmd - v" # faster than osascript
    rm $kitty_ls_file
fi

rm $pid_file