5

When working in vim, I like to have a toggle-able terminal window that I can bring with me from tab to tab. This works by tracking a buffer, and opening it in a new bot split (if it isn't visible), or hiding the split (if it is visible). However, my current solution only allows me to use a single terminal buffer at a time.

I am trying to write a set of functions that allows me to manage and toggle a "set" of terminal buffers, all of which are next to each other and at the bottom of the screen. My method so far is to store a list of each terminal window as it is opened, with something like:

[{'bufnr': 2, 'type': 'root'}, {'bufnr': 3, 'type': 'v'}, {'bufnr': 4, 'type': 'h'}]

I can then iterate through these and open them in the same order that they were originally opened. This is fine, except for when any of the buffers in the set is closed. There are a ton of different edge cases, so I've been looking for a better solution.

vim provides the function winlayout() that I could potentially filter down to only windows that are in the terminal set, but I am having an incredibly hard time parsing out the information that it returns.

Could someone help me figure out how to parse the output of winlayout() to reproduce the layout it defines? Or, if there is a better way to do this, I am always open to ideas.

1 Answer 1

7

Full restore

If you want to restore layout with multiple termina buffers, you must restore other windows and buffers too, so it's in fact a full buffer layout restore, it's usage is not limited to terminal buffers. Three things must be done to achive that:

  • Split windows according to saved layout. We can use result of winlayout() to do this.
  • Load buffer in each window. We must record bufnr in each window when we save winlayout().
  • Resize each window. We can use :h winrestcmd() to do this.

Two recursions are used to save and restore layout:

command SaveBufferLayout call s:save_buffer_layout()
command RestoreBufferLayout call s:restore_buffer_layout()

function s:save_buffer_layout() abort
  let s:buf_layout = winlayout()
  let s:resize_cmd = winrestcmd()
  call s:add_buf_to_layout(s:buf_layout)
endfunction

" add bufnr to leaf
function s:add_buf_to_layout(layout) abort
  if a:layout[0] ==# 'leaf'
    call add(a:layout, winbufnr(a:layout[1]))
  else
    for child_layout in a:layout[1]
      call s:add_buf_to_layout(child_layout)
    endfor
  endif
endfunction

function s:restore_buffer_layout() abort
  if !has_key(s:, 'buf_layout')
    return
  endif

  " create clean window
  new
  wincmd o

  " recursively restore buffers
  call s:apply_layout(s:buf_layout)

  " resize
  exe s:resize_cmd
endfunction

function s:apply_layout(layout) abort

  if a:layout[0] ==# 'leaf'
    if bufexists(a:layout[2])
      exe printf('b %d', a:layout[2])
    endif
  else

    " split cols or rows, split n-1 times
    let split_method = a:layout[0] ==# 'col' ? 'rightbelow split' : 'rightbelow vsplit'
    let wins = [win_getid()]
    for child_layout in a:layout[1][1:]
      exe split_method
      let wins += [win_getid()]
    endfor

    " recursive into child windows
    for index in range(len(wins) )
      call win_gotoid(wins[index])
      call s:apply_layout(a:layout[1][index])
    endfor

  endif
endfunction

It's possible to make it work for multiple layouts, you can the newest script here.

Original manual workaround

It's trouble and buggy to toggle multiple terminal windows, you can step back, split by manual, then load the hided terminal buffer by mark or buffer number.

You can create global mark for terminal buffer by <c-w>:mark X or <c-w>:kX, hide it, and load it back with 'X, here is a map that let you create mark with <c-w>m{capital letter}:

tnoremap <expr> <c-w>m printf('<c-w>:mark %s<cr>',  nr2char(getchar()))

Note that you can also combine the split and load together in a single command:

new +'X

You can also use :ls R or :ls F to list terminal buffer.


winlayout

:h winlayout()


        The result is a nested List containing the layout of windows
        in a tabpage.
                ...
        For a leaf window, it returns:
            ['leaf', {winid}]
        For horizontally split windows, which form a column, it
        returns:
            ['col', [{nested list of windows}]]
        For vertically split windows, which form a row, it returns:
            ['row', [{nested list of windows}]]

leaf item contains :h winid , it's an uique id across tabs.

This is a horizontally splited col, it has 3 leafs:

┌─────┐
│     │
├─────┤
│     │
├─────┤
│     │
└─────┘

This is a vertically splited row, it has 3 leafs:

┌────┬────┬────┐
│    │    │    │
└────┴────┴────┘

They can be nested, this layout is a row, it's splited into two col, each col is splited into two leafs:

 ┌──────┬──────┐
 │  1   │  3   │
 ├──────│──────┤
 │  2   │  4   │
 └──────┴──────┘

this layout is a col, it's splited into two row, each row is splited into two leafs:

 ┌──────┬──────┐
 │  1   │  2   │
 ├─────────────┤
 │  3   │  4   │
 └──────┴──────┘

Note that :h winnr() for above two layouts are different.

4
  • I wasn't aware of the methods you'd discussed for using mark which is pretty convenient and works similar to the methods that I use now. The real problem here is the logic surrounding some complex nesting of terminal buffer windows, hence why I'd looked into winlayout(). From what I understand there is no related function to recreate the return of a call to winlayout(), right? In this case, would my best bet to parsing this list structure be using recursion?
    – ThoseKind
    Jan 14, 2020 at 19:08
  • 1
    @ThoseKind See update, I've tested it with about 10 windows.
    – dedowsdi
    Jan 15, 2020 at 1:06
  • This is amazing man, thank you so much.
    – ThoseKind
    Jan 16, 2020 at 0:18
  • Amazing! I want the plugin to silently save stack of layouts on window splits and a mapping to restore the latest. Like emacs folks have. Will play with it, thx!
    – Maxim Kim
    Jan 21, 2020 at 6:52

Your Answer

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

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