Interactive Scripts with Vim
I suspect this style of Vim use is actually closer to Kakoune (kakoune.org) or Helix (helix-editor.com) than the way most people seem to use Vim. But I think it demonstrates the strength of the Unix philosophy and composable software in general.
Specifically, the tool I’ve just completed is a two-part editing and document creation "wizard" that shares duties between Vim and a Ruby script. The communication between the two is performed with command-line arguments, standard out, and exit status codes.
In short, my minor epiphany was going from:
vim -> hard script -> vim
To:
vim -> easy script -> vim -> easy script -> vim
Example: Creating book review pages
I’ve just started a section of the website I’ve been meaning to build for years: a place to self-host my book reviews:
(Prior to this, I’ve been contributing to Goodreads since 2008. Wow, that’s 15 years!)
I’ve been keeping a "reading journal" since 1992. (Another wow, that’s over 30 years!) It’s part of my wiki, but it’s in a fixed-width text format I settled on a while back.
Reading journal entries look like this, with four columns, each separated by at least two spaces. Very simple:
2024-04-13 Shell Script Pearls Ron Peters 318 2024-04-16 Piranesi Susanna Clarke 272 2024-04-17 Programming Ruby 3.3 Noel Rappin 718
After writing one of those entries, I’d like to add the book to my website and create a new page for a review.
(I did the first three by hand, but it’s a total pain to copy all of that information multiple times to create the page and the link to the page.)
The challenge
I need to accomplish two fairly simple things: Create a link and create a page. But the devil is in the details.
First, I want to extract all of the information from the journal entry. I’d like to do this from Vim by using the current line under the cursor.
when I create a new link on my books index page, the link needs to be inserted at the top of the current list.
I also want to be able to edit or otherwise change the file name of the page it will link to. I’ll auto-generate the filename from the title, but sometimes it’ll be too long or I might want to combine reviews for series, etc.
I don’t want the new page to be created until I’ve decided that I like the filename I’ve chosen.
Update: And I’ve also decided that I won’t necessarily write a review for every single book I’ve read. I really don’t feel the need to review, say, The Fellowship of the Ring, because what else can I say that hasn’t already been said? It would be super annoying to need to delete a review page I’m not going to use. Again, splitting this action into two steps gives me tons of flexibility.
Finally, I’ll use the filename and other book information I’ve gathered to generate the initial meta-info for the book review in a new page source file and open it in Vim for editing.
The solution
The ability to interactively edit the book review’s file name is the most challenging part of my desired process.
Thankfully, my very limited project time gave me plenty of "off" time away from the keyboard to realize that I could chop the process into two parts and let Vim be the interactive editor for the filename.
I mean, this whole thing is launched from Vim in the first place and the review is going to be written in Vim. Why not use it to edit the link (with the filename) as well, right?
The clever bit is that I still have just one external Ruby script, but it performs either half of the process by detecting the type of input it’s been given.
Here’s how it works:
-
With my cursor over one of my reading journal entries, I hit a shortcut sequence,
,wb
(mnemonic for "wiki book", following a convention I have for my wiki-related Vim shortcuts) -
The selected journal line is sent to a Ruby script, which inserts a new link (including a proposed filename based on the title) into my book index page
-
Vim opens the book index page for me
-
I edit the filename (if needed, which shouldn’t be too often)
-
Then I use the
,wb
shortcut sequence again on the book index link line! -
That line is sent to the same Ruby script, but this time it creates the new review page with the given file name
-
Vim opens the new review page and I’m ready to start writing.
It looks like a lot when I write it all out, but now it’s just a matter of seconds to go from my reading journal to writing a review in a new page.
Vim script
The Vim part is as small as I could make it. My philosophy these days is to go no-plugins with Vim and instead rely on external scripts as much as possible. I have zero regrets about this path.
You can see that I’ve created a function that runs my new-book
Ruby script with the currrent line’s text as a quoted argument to the script.
" Does two things: " * On journal, adds book entry to ratf/books page " * On ratf/books page, creates new page from filename function! NewBook() silent let foo = system("new-book '" . getline(".") . "'") if v:shell_error == 0 exec "edit " . foo else echo foo endif endfunction! nnoremap <leader>wb :call NewBook()<cr>
If the script returns error-free, then it will have written a filename to standard out (which I capture in variable foo
, LOL). That file is opened for editing.
This allows me to control Vim’s behavior from the Ruby script (and vice-versa) to perform two separate but similar actions with a single function and shortcut mapping.
Note: I started off sending the current line to the script’s stdin, but changed it to an argument when I thought I would need to prompt myself to approve/edit the new page’s filename. Now that I no longer need to do that, I could switch back to sending it as stdin, which would also avoid any quoting issues on the command line.
The Ruby script
The ruby portion is much more robust.
You can read the whole new-book
script in my Rat Tools
repo here: new-book.
I’m not going to go over every line, but you’ll notice right away that it’s split into two halves using regexp matching to determine if it’s been sent a reading journal line or a books page index line.
Depending on the input, it’ll generate the new link or page and return the filename I’d like Vim to open for me.
I’ve made every effort to prevent accidents. When the new link is inserted into the existing list, I write the update to a temporary file. I don’t replace the existing page with the temporary until I’ve confirmed that the new file is actually larger than the original. The script will also abort if I attempt to create a new review page but a file with that name already exists.
Conclusions
Splitting the action into two steps allows the process to be interactive when I’m not confident my auto-generated file name will always be what I want.
Dropping back into Vim between steps means that I have the full power of the text editor while I’m making changes.
A single script can perform both parts of the process by detecting what type of input it’s been given.
Tooling can prevent accidents better than doing things manually. …But only if you put in the extra effort to check for the right conditions.
Vim alone could do all of this, but it’s a billion times more pleasant to write (and maintain!) the functionality in Ruby. Another side-effect of having an external script, I’ve found, is that it’s more versitile in the long run: I can run it separately from Vim and I can borrow parts of it to write similar scripts outside of the editor.