Jobs and Timers in neovim: How to watch your builds fail
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
successCoolio.
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
endifSo 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)
endfunctionWe 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] . ", "
endfunctionThere 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
endfunctionNow 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)
endifAll 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 spaceAnd that's that. Find a lot more goodies in my dotfiles. Cheerios. Hugs n kisses and all that.

