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
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.