3

I'm new to vimscript and writing a function to toggle a terminal buffer. Here is what I tried:

let g:toggle_term = "<C-j>"

let g:term_buf_nr = -1
function! ToggleTerminal()
    if g:term_buf_nr == -1
        execute "bot term"
        let g:term_buf_nr = bufnr("$")
    else
        execute "bd! " .g:term_buf_nr
        let g:term_buf_nr = -1
    endif
endfunction

execute "nnoremap ".g:toggle_term ." :call ToggleTerminal()<CR>"

The function works fine and toggles the terminal buffer only in case the terminal buffer itself is not focused. If it is focused then pressing the g:toggle_term combination does nothing.

Is there a way make g:toggle_term to toggle terminal even if the terminal buffer is currently focused?

5
  • 2
    Don't you need to also create a tnoremap to have the mapping executed? :h :tnoremap
    – statox
    Nov 12, 2019 at 17:19
  • @statox Not sure, because this will send a command to the terminal itself.
    – St.Antario
    Nov 12, 2019 at 17:45
  • 1
    "Not sure" then read :h Terminal-Job. nnoremap only applies in Terminal-Normal mode.
    – Matt
    Nov 12, 2019 at 19:13
  • @Matt There is no such manual entry. Did you mean Terminal-mode?
    – St.Antario
    Nov 13, 2019 at 6:39
  • @Matt execute "tnoremap ".g:toggle_term ." exit<CR> :call ToggleTerminal<CR>" does not really call the function.
    – St.Antario
    Nov 13, 2019 at 6:53

3 Answers 3

2

As @statox suggested in comments I should have used tnoremap to implement it. The following works fine:

execute "tnoremap ".g:toggle_term ." <C-w>:call ToggleTerminal()<CR>"

The mistake I initially made was the space right after the N

execute "tnoremap ".g:toggle_term ." <C-w>N :call ToggleTerminal()<CR>"

and since I rebound leader to <space> it did not work the way I wanted.

Also, the declaration

let g:term_buf_nr = -1

should be script private and replaced with

let s:term_buf_nr = -1
4
  • 1
    No need N, there is :h ctrl-w_:
    – dedowsdi
    Nov 13, 2019 at 8:24
  • @dedowsdi Could you please expand? There is a manual entry specifying that C-w N is the combination to switch to a normal mode. :h terminal-use
    – St.Antario
    Nov 13, 2019 at 8:29
  • 1
    :h ctrl-w_: is a valid command in vim, execute it, you should see everything.
    – dedowsdi
    Nov 13, 2019 at 8:30
  • @dedowsdi Really, thanks.
    – St.Antario
    Nov 13, 2019 at 8:40
1

I slightly refactored your function and made it more robust. After quitting the terminal, with for example exit command in Windows, you will get an error when trying to invoke your ToggleTerminal() function.

let s:term_buf_nr = -1
function! s:ToggleTerminal() abort
    if s:term_buf_nr == -1
        execute "botright terminal"
        let s:term_buf_nr = bufnr("$")
    else
        try
            execute "bdelete! " . s:term_buf_nr
        catch
            let s:term_buf_nr = -1
            call <SID>ToggleTerminal()
            return
        endtry
        let s:term_buf_nr = -1
    endif
endfunction

nnoremap <silent> <Leader>t :call <SID>ToggleTerminal()<CR>
tnoremap <silent> <Leader>t <C-w>N:call <SID>ToggleTerminal()<CR>

By enclosing the else part of the code in try-catch block, you can react to the aforementioned problem accordingly. I bet you can also let s:term_buf_nr on even of closing the terminal, but I don't know how to do it (I didn't bother, as this solution works for me just fine).

The only thing I don't like about it yet is the fact that it destroys the buffer instead of hiding it somehow, but that's different topic :)

0

I was looking for a robust way to toggle a terminal and ended up building something that meets my expectations.

  • Off: any visible terminal is hidden
  • On: terminal as a bottom panel (restore first hidden terminal or create new one)
  • Works with already open terminal
  • Arguments: size and side of the terminal, defaults to 6 lines at the bottom

It seems pretty robust, but any idea to improve it is welcome.

function! PutTermPanel(buf, side, size) abort
  " new term if no buffer
  if a:buf == 0
    term
  else
    execute "sp" bufname(a:buf)
  endif
  " default side if wrong argument
  if stridx("hjklHJKL", a:side) == -1
    execute "wincmd" "J"
  else
    execute "wincmd" a:side
  endif
  " horizontal split resize
  if stridx("jkJK", a:side) >= 0
    if ! a:size > 0
      resize 6
    else
      execute "resize" a:size
    endif
    return
  endif
  " vertical split resize
  if stridx("hlHL", a:side) >= 0
    if ! a:size > 0
      vertical resize 6
    else
      execute "vertical resize" a:size
    endif
  endif
endfunction

function! s:ToggleTerminal(side, size) abort
  let tpbl=[]
  let closed = 0
  let tpbl = tabpagebuflist()
  " hide visible terminals
  for buf in filter(range(1, bufnr('$')), 'bufexists(bufname(v:val)) && index(tpbl, v:val)>=0')
    if getbufvar(buf, '&buftype') ==? 'terminal'
      silent execute bufwinnr(buf) . "hide"
      let closed += 1
    endif
  endfor
  if closed > 0
    return
  endif
  " open first hidden terminal
  for buf in filter(range(1, bufnr('$')), 'bufexists(v:val) && index(tpbl, v:val)<0')
    if getbufvar(buf, '&buftype') ==? 'terminal'
      call PutTermPanel(buf, a:side, a:size)
      return
    endif
  endfor
  " open new terminal
  call PutTermPanel(0, a:side, a:size)
endfunction



nnoremap <silent> yot :call <SID>ToggleTerminal('J', 6)<CR>
2
  • That looks great, though for vim (I use Mac) while closing the buffer I need to press an extra key after the mapping to close it. I am using <leader>t to both open and close. Would you know how to get around that? Also terminal opens in again in insert mode, and one needs to press i to actually use it. One more feature that would be handy is to put the focus back in the window from where the terminal was invoked... that's especially true for sessions with many windows/splits. Thanks!
    – codepoet
    Jan 27, 2021 at 0:23
  • Hi! Unfortunately I don't have a Mac to test that. Terminals might be handled differently on Linux. Also, check whether your Vim is up to date, as the pre-installed version on Mac can be quite old. You could try to use feedkeys() to input i, which should enter 'insert mode'. I know use as updated version of this script, which actually uses feedkeys() (as a workaround for another problem). If you use it, map it with nmap, e.g. nmap yot <Plug>(TermToggle)
    – Biggybi
    Jan 27, 2021 at 1:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.