TITLE: Replacing the lightline.vim plugin with a custom
statusline/tabline
DATE: 2018-01-11
AUTHOR: John L. Godlee
====================================================================


Yesterday I decided, in an effort to remove plugin dependencies in
my .vimrc and to learn a bit more about how VIM works, to get rid
of lightline.vim and replace it with a statusline and tabline of my
own design.

 [lightline.vim]: https://github.com/itchyny/lightline.vim

Statusline basics

I'v never had any problems with lightline in the year or so that
I've been using it. I've never used VIM without lightline in fact.
Lightline provides lots of useful information as standard, such as
the file name, the cursor position in the file, the file type, and
importantly, it changes colour depending on what mode you are in,
insert, visual, normal, etc..


![Lightline](https://johngodlee.xyz/img_full/lightline/lightline.png
)

 ![Lightline insert
mode](https://johngodlee.xyz/img_full/lightline/lightline_insert.png
)

But, just having lightline do everything for me isn't particularly
exciting. So first thing was to see what the default statusline
looks like:

 ![Default status
line](https://johngodlee.xyz/img_full/lightline/status_basic.png)

Very boring, all you can see is the file name.

Ideally, I want some useful information about my document in the
statusline, namely in order from most to least wanted:

-   What mode I am in
-   The file name
-   The git branch the file is on
-   Whether the file has been modified since the last save
-   The file type, e.g. markdown, java, html
-   How far through the document I am, as a percentage
-   How far through the document I am, as line number
-   The column number

All of these, except the git branch, can be easily added to the
statusline with code found in the help file for the statusline,
(:help statusline)

To add an item to the statusline, in your .vimrc add a line that
starts:

   set statusline=

Then add one of the 'printf' style codes in the help file. e.g. for
the file name, otherwise known as the tail of the filepath (%t):

   set statusline=%t

After reloading your .vimrc (:source .vimrc) you will see nothing
has changed. Yay!

To test it's actually worked, try adding some other codes:

   set statusline=%t%m%y%p%l%c

which produces this:

 ![Status line with
content](https://johngodlee.xyz/img_full/lightline/status_codes.png)

which is very ugly, and not particularly easy to read, either in
the vimrc or in the statusline itself. First, let's rectify this in
the .vimrc. I like to separate out my statusline code so that each
item or group of items is on its own line. The syntax used below is
similar to that used to design a prompt in the bash shell:

   set statusline=%t   " file name
   set statusline+=%m  " modified?
   set statusline+=%y  " file type
   set statusline+=%p  " percentage through file
   set statusline+=%l  " line number
   set statusline+=%c  " column number

Source your .vimrc to see that nothing has changed in the
statusline, but the .vimrc is much easier to read.

Now we can break up the information in the statusline a bit, using
spaces and special characters.

First, I want a space between certain items, which I can add by
adding set statusline+=\ lines to my statusline code. Note the
actual space after the \. You can also add normal characters to the
statusline. For instance, to break up the line and column number
you could add a : by adding set statusline+=::

   set statusline=%t   " file name
   set statusline+=\   " space
   set statusline+=%m  " modified?
   set statusline+=\   " space
   set statusline+=%y  " file type
   set statusline+=\   " space
   set statusline+=%p  " percentage through file
   set statusline+=\   " space
   set statusline+=%l  " line number
   set statusline+=:   " colon separator
   set statusline+=%c  " column number

 ![Status line with spacing between
content](https://johngodlee.xyz/img_full/lightline/status_space.png)

Now. I don't really want the information about my position in the
file on the left side, I'd rather have it on the right side. I can
switch to the right side of the statusline by adding set
statusline+=%= before the lines you want to appear on the right
side:

   set statusline=%t   " file name
   set statusline+=\   " space
   set statusline+=%m  " modified?
   set statusline+=\   " space
   set statusline+=%y  " file type
   set statusline+=\   " space

   set statusline+=%=      " switch to right side

   set statusline+=%p  " percentage through file
   set statusline+=\   " space
   set statusline+=%l  " line number
   set statusline+=:   " colon separator
   set statusline+=%c  " column number

 ![Status line split left and
right](https://johngodlee.xyz/img_full/lightline/status_right.png)

So that's the basics of a statusline covered I think. Now onto
making our own functions

Custom items and functions in statusline

I didn't come up with the ideas presented here myself, I adapted
them from online sources. Three things on my list of desired
features in the statusline don't appear as standard codes in the
help file, those things are displaying the mode I am in, changing
the colour based on the mode, and the git branch.

Display the mode

First let's do the mode, which is fairly easy, because VIM already
has a global variable called currentmode, so I just need to give
the default outputs some aliases that are easier to read:

   let g:currentmode={
       \ 'n'  : 'Normal',
       \ 'no' : 'N·Operator Pending',
       \ 'v'  : 'Visual',
       \ 'V'  : 'V·Line',
       \ '^V' : 'V·Block',
       \ 's'  : 'Select',
       \ 'S'  : 'S·Line',
       \ '^S' : 'S·Block',
       \ 'i'  : 'Insert',
       \ 'R'  : 'R',
       \ 'Rv' : 'V·Replace',
       \ 'c'  : 'Command',
       \ 'cv' : 'Vim Ex',
       \ 'ce' : 'Ex',
       \ 'r'  : 'Prompt',
       \ 'rm' : 'More',
       \ 'r?' : 'Confirm',
       \ '!'  : 'Shell',
       \ 't'  : 'Terminal'
       \}

then call the variable in the statusline code:

   set statusline=%{g:currentmode[mode()]} " mode
   set statusline+=\   " space
   set statusline+=%t  " file name
   set statusline+=\   " space
   set statusline+=%m  " modified?
   set statusline+=\   " space
   set statusline+=%y  " file type
   set statusline+=\   " space

   set statusline+=%=      " switch to right side

   set statusline+=%p  " percentage through file
   set statusline+=\   " space
   set statusline+=%l  " line number
   set statusline+=:   " colon separator
   set statusline+=%c  " column number

Git branch

The git branch is a bit harder, requiring vim script and running
some shell commands in the .vimrc. Here is my code to define a
function to show the git branch in the statusline:

   function CurrentGitBranch()
       let gitoutput = system('git status -b
'.shellescape(expand('%')).' | head -1 | grep -oE "[^ ]+$" | tr -d
"[:cntrl:]"')
       if gitoutput =~ "invalid"
           let b:gitstatus = ''
       else
           let b:gitstatus = gitoutput
       endif
   endfunc

   autocmd BufEnter,BufWritePost * call CurrentGitBranch()

Let's go through this line by line.

-   function CurrentGitBranch() defines a function called
CurrentGitBranch().
-   let gitoutput = system('git status -b
'.shellescape(expand('%')).' | head -1 | grep -oE "[^ ]+$" | tr -d
"[:cntrl:]"') defines a variable called gitoutput.
   -   It uses the system() command to run git status -b using the
open file .shellescape(expand('&')) as the file to runthe command
on.
   -   | head -1 | grep -oE "[^ ]+$" | tr -d "[:cntrl:]" is a
series of pipes which returns the first line of git status then
greps the final word, then removes the ^@ control sequence, which
would otherwise appear in the statusline.
-   if gitoutput =~ "invalid" is the start of an if statement which
defines b:gitstatus as either an empty string if no file is open
(which would normally return a line containing the word invalid, or
defines it as the contents of gitoutput.
-   autocmd BufEnter,BufWritePost * call CurrentGitBranch() runs
the function.

To add b:gitstatus to the statusline, follow the same rules as for
the current mode function we did earlier.

   set statusline=%{g:currentmode[mode()]} " mode
   set statusline+=\   " space
   set statusline+=%t  " file name
   set statusline+=\   " space
   set statusline+=%{b:gitstatus}      " git branch
   set statusline+=%m  " modified?
   set statusline+=\   " space
   set statusline+=%y  " file type
   set statusline+=\   " space

   set statusline+=%=      " switch to right side

   set statusline+=%p  " percentage through file
   set statusline+=\   " space
   set statusline+=%l  " line number
   set statusline+=:   " colon separator
   set statusline+=%c  " column number

 ![Status line with git
repository](https://johngodlee.xyz/img_full/lightline/status_git.png
)

Conditional colour changing

For the colour changing status bar you can use another function:

   function! ChangeStatuslineColor()
     if (mode() ==# 'i')
       exe 'hi User1 ctermfg=black ctermbg=white'
     else
       exe 'hi User1 ctermfg=white ctermbg=black'
     endif
     return ''
   endfunction

This defines the User1 colour palette, with foreground (text) and
background colours, reversing the colours depending on the current
mode. To add it to the statusline:

   set statusline=%{ChangeStatuslineColor()}   " Load function
   set statusline+=%1* " Change colour palette to `User1`
   set statusline+=%{g:currentmode[mode()]}    " Display the mode
   set statusline+=%0* " Return to default colour palette
   set statusline+=\   " space
   set statusline+=%t  " file name
   set statusline+=\   " space
   set statusline+=%{b:gitstatus}      " git branch
   set statusline+=%m  " modified?
   set statusline+=\   " space
   set statusline+=%y  " file type
   set statusline+=\   " space

   set statusline+=%=      " switch to right side

   set statusline+=%p  " percentage through file
   set statusline+=\   " space
   set statusline+=%l  " line number
   set statusline+=:   " colon separator
   set statusline+=%c  " column number

 ![Status line with
colour](https://johngodlee.xyz/img_full/lightline/status_colour.png)

Obviously you can change the colours to whatever you desire, using
ANSI colour codes to extend the colour range, and if you have an X
terminal, just change cterm to xterm and you can then use Xterm
colour codes. You could also add extra elseif statements to add
other modes. Lightline for example turns orange when in visual mode.

My actual statusline

While the above has all the basic functionality, it's very ugly.
For my real statusline I tweaked things a little bit more:

   " statusline always showing, even when NERDTree is hidden
   set laststatus=2

   " Map of modes and their codes for statusline
   let g:currentmode={
       \ 'n'  : 'Normal',
       \ 'no' : 'N·Operator Pending',
       \ 'v'  : 'Visual',
       \ 'V'  : 'V·Line',
       \ '^V' : 'V·Block',
       \ 's'  : 'Select',
       \ 'S'  : 'S·Line',
       \ '^S' : 'S·Block',
       \ 'i'  : 'Insert',
       \ 'R'  : 'R',
       \ 'Rv' : 'V·Replace',
       \ 'c'  : 'Command',
       \ 'cv' : 'Vim Ex',
       \ 'ce' : 'Ex',
       \ 'r'  : 'Prompt',
       \ 'rm' : 'More',
       \ 'r?' : 'Confirm',
       \ '!'  : 'Shell',
       \ 't'  : 'Terminal'
       \}

   " Change statusline colour based on mode
   function! ChangeStatuslineColor()
     if (mode() ==# 'i')
       exe 'hi StatusLine ctermbg=black ctermfg=032'
     elseif (mode() =~# '\v(v|V)')
       exe 'hi StatusLine ctermbg=black ctermfg=172'
     else
       exe 'hi Statusline ctermbg=white ctermfg=black'
     endif
     return ''
   endfunction

   " Get git branch in statusline
   function CurrentGitBranch()
       let gitoutput = system('git status -b
'.shellescape(expand('%')).' | head -1 | grep -oE "[^ ]+$" | tr -d
"[:cntrl:]"')
       if gitoutput =~ "invalid"
           let b:gitstatus = ''
       else
           let b:gitstatus = gitoutput
       endif
   endfunc

   autocmd BufEnter,BufWritePost * call CurrentGitBranch()

   " Statusline
   " left side
   set statusline=%{ChangeStatuslineColor()}   " Change colour
   set statusline+=\ %-8.{toupper(g:currentmode[mode()])}  "
Current mode
   set statusline+=\ \|\   " Vert-line and space
   set statusline+=%t  " File name
   set statusline+=\ \|\   " Vert-line and space
   set statusline+=%{b:gitstatus}      " git branch
   set statusline+=%=  " Switch to right side

   " right side
   set statusline+=%m%r " Modified and read only flags
   set statusline+=\       "Space
   set statusline+=%y  " File type
   set statusline+=\ \|\   " Space, Vert-line and space
   set statusline+=%3.p%%  " Percentage through file - min size 3
   set statusline+=\ \|\   " Vert-line and Space
   set statusline+=%8.(%4.l:%-3.c%)    " Line and column number in
group
   set statusline+=\       " Space

 ![My status
line](https://johngodlee.xyz/img_full/lightline/status_my.png)

 ![My status line - insert
mode](https://johngodlee.xyz/img_full/lightline/status_my_insert.png
)

The tabline

Less important, but still something that lightline used to do which
now I do myself is the tabline. With lightline the tabline looks
like this:

 ![Lightline tab
line](https://johngodlee.xyz/img_full/lightline/tab_lightline.png)

Setting the tabline manually isn't as easy as the statusline.
Technically you can set the tabline using the same code as the
statusline (e.g. set tabline=%t), but this will result in the
following, regardless of how many tabs are actually open:

 ![Default tab
line](https://johngodlee.xyz/img_full/lightline/tab_simple.png)

I don't entirely understand how the tabline works at the moment, so
I borrowed this tabline function from this website:

 [this website]:
http://dhruvasagar.com/2014/04/06/vim-custom-tabline

   function! MyTabLine()
     let s = ''
     for i in range(tabpagenr('$'))
       let tabnr = i + 1 " range() starts at 0
       let winnr = tabpagewinnr(tabnr)
       let buflist = tabpagebuflist(tabnr)
       let bufnr = buflist[winnr - 1]
       let bufname = fnamemodify(bufname(bufnr), ':t')

       let s .= '%' . tabnr . 'T'
       let s .= (tabnr == tabpagenr() ? '%#TabLineSel#' :
'%#TabLine#')
       let s .= ' ' . tabnr

       let n = tabpagewinnr(tabnr,'$')
       if n > 1 | let s .= ':' . n | endif

       let s .= empty(bufname) ? ' [No Name] ' : ' ' . bufname . '
'

       let bufmodified = getbufvar(bufnr, "&mod")
       if bufmodified | let s .= '+ ' | endif
     endfor
     let s .= '%#TabLineFill#'
     return s
   endfunction

Then to run it:

   set tabline=%!MyTabLine()

 ![My
tabline](https://johngodlee.xyz/img_full/lightline/tab_function.png)