noobmaster69
noobmaster69

Reputation: 3115

Show mode in statusline in Vim

I've developed a nice customisable statusline in Vim however I'm having real trouble displaying the current mode I'm working in within it.

I use a dictionary to dictate what options to display via the left and right of the statusline:

" Display Options {{{
let s:statusline_options = {
            \   'active': {
            \     'left':  [ 'readonly', 'mode' , 'git' ],
            \     'right': [ 'time', 'project' ],
            \   },
            \   'components': {
            \     'readonly': 'Statusline_readonly()',
            \     'mode': 'Statusline_mode()',
            \     'git': 'Statusline_git()',
            \     'time': "strftime(%a\ %d\ %b\ %H:%M)",
            \     'project': 'Statusline_project()'
            \   },
            \   'seperators': {
            \     'readonly': ' %s',
            \     'mode': '%s >',
            \     'git': ' %s',
            \     'time': ' < ',
            \     'project': '[%s] '
            \   },
            \   'components_to_color': {
            \     'mode': 1,
            \     'project': 1
            \   },
            \   'theme_colors': {
            \     'default': [ '#abb2bf', '#61afef', '#98c379' ],
            \     'onedark': [ '#abb2bf', '#61afef', '#98c379' ],
            \     'materialbox': [ '#1d272b', '#fb8c00', '#43a047']
            \   },
            \   'mode_map': {
            \     'n': 'NORMAL', 'i': 'INSERT', 'R': 'REPLACE', 'v': 'VISUAL', 'V': 'VISUAL', "\<C-v>": 'V-BLOCK',
            \     'c': 'COMMAND', 's': 'SELECT', 'S': 'S-LINE', "\<C-s>": 'S-BLOCK', 't': 'TERMINAL'
            \   },
            \ }
" }}}

I then piece together the creation of the statusline by introducing color groups and separators and calling the functions which the user specified as Components in the dictionary above:

" Statusline Functions {{{
function! StatuslineComponents(side) abort
    let output = ''

    " Fetch the components in the statusline dictionary
    for v in Fetch('active', a:side)
        let method = split(Fetch('components', v), '(')
        let component_color = ''

        " Check if the item should be specifically coloured
        if len(method) > 1 && method[1] != ')'
            let output .= StatuslineColor(v) . StatuslineFormat(v, method[0], method[1])
        else
            let output .= StatuslineColor(v) . StatuslineFormat(v, method[0])
        endif
    endfor

    return output
endfunction

function! StatuslineColor(component)
    for v in keys(s:statusline_options.components_to_color)
        if v == a:component
            return '%#Component_' . v . '#'
        endif
    endfor

    return '%#ComponentDefault#'
endfunction

function! StatuslineFormat(...)
    let output = ''
    let seperator = Fetch('seperators', a:1)

    if a:0 > 2
        for param in split(a:3, ',')
            let value = call(a:2, [ param ], {})
        endfor
    else
        let value = call(a:2, [], {})
    endif

    " Remove any last )'s from the value
    let value = substitute(value, ')\+$', '', '')

    if seperator =~ '%s'
        let output = printf(seperator, value)
    else
        let output = value . seperator
    endif

    return output
endfunction

function! ChangeStatuslineColor() abort
    let s:mode_colors = []

    try
        for mode_color in Fetch('theme_colors', g:colors_name)
            let s:mode_colors += [mode_color]
        endfor
    catch
        try
            for mode_color in Fetch('theme_colors', 'default')
                let s:mode_colors += [mode_color]
            endfor
        catch
            let s:mode_colors = ['#e06c75'] + ['#e06c75'] + ['#e06c75']
        endtry
    endtry

    if (mode() ==# 'i')
        exec printf('hi ComponentDefault guifg=%s', s:mode_colors[1])
    elseif (mode() =~# '\v(v|V)')
        exec printf('hi ComponentDefault guifg=%s', s:mode_colors[2])
    else
        exec printf('hi ComponentDefault guifg=%s', s:mode_colors[0])
    endif

    for component in keys(s:statusline_options.components_to_color)
        if (mode() ==# 'i')
            exec printf('hi Component_%s guifg=%s', component, s:mode_colors[1])
        elseif (mode() =~# '\v(v|V)')
            exec printf('hi Component_%s guifg=%s', component, s:mode_colors[2])
        else
            exec printf('hi Component_%s guifg=%s', component, s:mode_colors[0])
        endif
    endfor

    return ''
endfunction

" Fetch a value from the Statusline options
function! Fetch(key, value) abort
    return get(s:statusline_options[a:key], a:value, '')
endfunction

" }}}

My custom functions to be included in the Statusline, along with the key Statusline_mode() function are written below:

" Component Functions {{{

" Show the mode in the statuslin
function! Statusline_mode() abort
    return get(s:statusline_options.mode_map, mode(), '')
endfunction

function! Statusline_project() abort
    return GetCurrentSite(g:code_dir)
endfunction

function! Statusline_git() abort
    let git = fugitive#head()
    if git != ''
        return ' '.fugitive#head()
    else
        return ''
    endif
endfunction

function! Statusline_readonly() abort
    if &readonly || !&modifiable
        return ' '
    else
        return ''
    endif
endfunction

" }}}

The statusline is then set via the command group below:

" Set The Statusline {{{
augroup statusline
    hi StatusLine guibg=NONE gui=NONE
    autocmd!
    autocmd WinEnter,BufWinEnter,FileType,ColorScheme,SessionLoadPost
                \ * let &l:statusline = StatuslineComponents('left') . '%=' . StatuslineComponents('right') |
                \ call ChangeStatuslineColor()
augroup end

" }}}

I have tried using InsertEnter command group and that does not seem to do the trick.

Upvotes: 1

Views: 8092

Answers (1)

Ingo Karkat
Ingo Karkat

Reputation: 172570

Let's start with your more minimalistic case:

let &l:statusline = mode() . ' ' . StatuslineComponents('left') . '%=' . StatuslineComponents('right')

This invokes mode() once, when defining the 'statusline'; it never updates, and the value is stuck at n.

To fix this, you need to use the %{...} item to evaluate the expression whenever the statusline is updated:

let &l:statusline = '%{mode()} ' . StatuslineComponents('left') . '%=' . StatuslineComponents('right')

Now back to your complete case. It's the same problem:

autocmd WinEnter,BufWinEnter,FileType,ColorScheme,SessionLoadPost
            \ * let &l:statusline = StatuslineComponents('left') . '%=' . StatuslineComponents('right') |
            \ call ChangeStatuslineColor()

Although you update on certain :autocmd events, none are triggered on mode change, so you always see the same mode. In any case, as :help mode() tells you, the correct value can only be obtained from the mentioned statusline expression.

As a first step to fix this, I would drop the :autocmd altogether and put everything in a statusline expression:

let &statusline = "%{StatuslineComponents('left')}%=%{StatuslineComponents('right')}"

If you run into performance issues, I would extract the slow elements (but not the mode() call!) into window-local variables, reference them from the statusline expression, and update them with :autocmd.

autocmd WinEnter,BufWinEnter,FileType,ColorScheme,SessionLoadPost * let w:left = StatuslineComponents('left')
let &statusline = "%{w:left}%=%{StatuslineComponents('right')}"

Upvotes: 6

Related Questions