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

8

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.

3
  • 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? Commented Jan 14, 2020 at 19:08
  • 1
    @ThoseKind See update, I've tested it with about 10 windows. Commented Jan 15, 2020 at 1:06
  • 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! Commented Jan 21, 2020 at 6:52

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.