Jobs and Timers in neovim: How to watch your builds fail

Fernando Mendes
Jun 7 · 4 min read

If you’re like me (and for your own sake, I truly hope you are not), you probably tend to have a lot of builds fail. Even worse, if you really are like me, you spend most of your time in vim.

If that is not the case, you’re in the clear, there’s nothing wrong with you, feel free to go, end this blog post now, be free, happy, enjoy the sunlight and the birds and the trees. Life is good.

.

.

.

.

.

.

.

.

.

.

.

.

… Are we, the sadists, all alone now? Cool. Ok, so you use vim a lot and you make builds fail. Chances are you would like to know when that happens without ever leaving vim. It’s alright. I got you, mate.

Here’s an asciicast of my nvim. Notice how the status bar includes, on the bottom right, the status of the CI. Notice how it updates. Damn, that’s neat. You want that.

First things first, either make an API wrapper, preferably in Rust or Go, something compiled and fancy, that allows you to check the GitHub checks API. Got it? Good. Now stop being a muppet and use hub instead.

Now that you have `hub`, you can make use of the `hub ci-status` command.

$ hub ci-status
success

Coolio.

Now let’s change our custom status bar.

First, we want to check if we’re in a git project:

let s:in_git = system(“git rev-parse — git-dir 2> /dev/null”)if s:in_git == 0
“ call hub
endif

So now we need to call `hub`. However just doing a `system` call to `hub` would be a blocking operation and we don’t want our vim to block every few moments for like 5 seconds. So let’s use `jobstart`.

Start by calling `:h jobstart` from your (n)vim. You can see that it runs an asynchronous job and it supports shell commands.

So let’s create a `CiStatus` function that looks like this:

function! CiStatus()
let l:callbacks = {
\ ‘on_stdout’: function(‘OnCiStatus’),
\ }
call jobstart(‘hub ci-status’, l:callbacks)
endfunction

We define a map of callbacks for `stdout` and delegate that to a new function called `OnCiStatus`. This is a very simple function that gets the output from `hub` and converts it to whatever we want, storing it in a `g:ci_status` variable. We will later use this variable in our statusline.

function! OnCiStatus(job_id, data, event) dict
if a:event == “stdout” && a:data[0] != ‘’
let g:ci_status = ParseCiStatus(a:data[0])
endif
endfunction
function! ParseCiStatus(out)
let l:states = {
\ ‘success’: “ci passed”,
\ ‘failure’: “ci failed”,
\ ‘neutral’: “ci yet to run”,
\ ‘error’: “ci errored”,
\ ‘cancelled’: “ci cancelled”,
\ ‘action_required’: “ci requires action”,
\ ‘pending’: “ci running”,
\ ‘timed_out’: “ci timed out”,
\ ‘no status’: “no ci”,
\ }
return l:states[a:out] . “, “
endfunction

There are a couple of things missing though. This runs the `hub ci-status` job only once. We want to have it perform constant checks. If we do `:h timers`, we can see the new `time` API in neovim. Theres a `timer_start` that takes a period and a callback to run after that period.

We can then change our `OnCiStatus` function to call `timer_start` with that first `CiStatus` function again:

function! OnCiStatus(job_id, data, event) dict
if a:event == “stdout” && a:data[0] != ‘’
let g:ci_status = ParseCiStatus(a:data[0])
call timer_start(30000, ‘CiStatus’) “ relevant new part
endif
endfunction

Now `CiStatus` gets called by `timer_start` every 3 seconds. `timer_start`, however, passes the `timer_id` as an argument to the callback. So we will need to modify `CiStatus` to accept an argument (that we can safely ignore):

function! CiStatus(timer_id)
let l:callbacks = {
\ ‘on_stdout’: function(‘OnCiStatus’),
\ }
call jobstart(‘hub ci-status’, l:callbacks)
endfunction
“ We also need to change the first CiStatus call to receive an int
“ Since we don’t care about it, let’s just use 0
let s:in_git = system(“git rev-parse — git-dir 2> /dev/null”)if s:in_git == 0
call CiStatus(0)
endif

All that’s missing now is to take the value of `g:ci_status` and put into the statusline. That’s pretty simple, using some code borrowed from Kade Killary.

set statusline=
set statusline+=\ \ \ “ Empty space
set statusline+=%< “ Where to truncate line
set statusline+=%f “ Path to the file in the buffer, as typed or relative to current directory
set statusline+=%{&modified?’\ +’:’’}
set statusline+=%{&readonly?’\ ’:’’}
set statusline+=%= “ Separation point between left and right aligned items
set statusline+=\ %{g:ci_status} “ Our custom CI status check
set statusline+=col:\ %c
set statusline+=\ \ \ “ Empty space

And that’s that. Find a lot more goodies in my dotfiles. Cheerios. Hugs n kisses and all that.

Subvisual

We design and develop thoughtful web & mobile products

Fernando Mendes

Written by

Elixir developer, doing distributed systems, P2P and blockchain at @subvisual. Traveling around with a Fuji XT-10 and documenting it.

Subvisual

Subvisual

We design and develop thoughtful web & mobile products