Vim Terminal Integration: Complete Guide
For decades, developers have debated the ideal workflow between terminal multiplexers like tmux and editors like Vim. Modern Vim (version 8.1+) and Neovim have largely settled this debate by introducing built-in terminal integration. Instead of constantly suspending Vim with :sh or juggling external terminal panes, you can now run shells, build tools, REPLs, and even full TUI applications directly inside Vim buffers.
This guide walks through what Vim terminal integration is, why it matters, how to configure and use it effectively, and the best practices that will keep your workflow fast and predictable.
What Is Vim Terminal Integration?
Vim terminal integration is a feature that embeds a pseudo-terminal (PTY) inside a Vim buffer. The buffer behaves like a regular terminal emulator: it accepts keystrokes, renders ANSI escape sequences, and runs interactive programs. Because the terminal lives in a buffer, it inherits all of Vim's window management, split layout, and buffer-switching capabilities.
Neovim was the first to ship a robust terminal implementation, and Vim 8.1 followed with its own terminal feature. While the APIs differ slightly, the core concepts are identical: a terminal is a special buffer mode, typically entered with terminal-mode, and controlled with a small set of commands and mappings.
Why It Matters
- Context preservation: Your editor state, splits, and registers remain intact while you run commands.
- Faster feedback loops: Run tests, linters, and builds in a split without leaving Vim.
- REPL-driven development: Send code to a language shell living in an adjacent terminal buffer.
- Reduced context switching: No need to remember tmux keybindings or manage separate windows.
- Scriptability: Terminals are buffers, so you can automate them with Vimscript or Lua.
Checking Your Version
Before using terminal integration, confirm your Vim supports it. Run the following inside Vim:
:echo has('terminal')
If the output is 1, you're good to go. For Neovim, terminal support is always available. You can also check the version:
:version
Look for +terminal in the feature list. If you see -terminal, you'll need to install a Vim build compiled with terminal support, or switch to Neovim.
Opening a Terminal
The simplest way to open a terminal in Vim is with the :terminal command (often abbreviated as :term). By default, this opens a horizontal split containing your shell:
:terminal
You can also specify a program to run instead of the default shell:
:terminal python3
:term npm run dev
:term git log --oneline
To control the split direction, prefix the command with a modifier:
:vertical terminal " open in a vertical split
:below terminal " open below the current window
:tab terminal " open in a new tab
In Neovim, the equivalent commands are the same, but you can also use :split | term or :vsplit | term for explicit layouts.
Terminal Mode vs Normal Mode
When you enter a terminal buffer, Vim places you in terminal mode. In this mode, almost every keystroke is sent directly to the underlying program, just like in a real terminal. To return to Normal mode and use Vim commands, you must press the escape key combination.
By default in Vim 8.1+, the key to leave terminal mode is Ctrl-\ Ctrl-n. In Neovim, the default is <C-\><C-n> as well, though many users remap <Esc>.
A common configuration makes escaping feel natural:
" In your ~/.vimrc or init.vim
tnoremap <Esc> <C-\><C-n>
tnoremap <M-[> <Esc>
tnoremap <C-v><Esc> <Esc>
The last mapping allows you to send a literal Esc to the terminal program when needed by prefixing it with Ctrl-v.
Navigating Between Windows
Once you exit terminal mode, you can move between splits using your normal window navigation mappings. However, while in terminal mode, those mappings won't work because keystrokes go to the shell. A useful pattern is to map terminal-mode navigation keys:
tnoremap <C-h> <C-\><C-n><C-w>h
tnoremap <j> <C-\><C-n><C-w>j
tnoremap <C-k> <C-\><C-n><C-w>k
tnoremap <C-l> <C-\><C-n><C-w>l
This lets you hop between splits without first leaving terminal mode explicitly.
Sending Text to a Terminal
One of the most powerful workflows is sending lines or visual selections from a code buffer to a running terminal — perfect for REPLs and ad-hoc scripting. Vim doesn't ship a built-in "send to terminal" command, but it's easy to add one.
Here's a Vimscript function that sends the current line or visual selection to the most recently used terminal buffer:
function! SendToTerminal(type = '') abort
if empty(a:type)
let l:mode = mode()
if l:mode ==# 'v' || l:mode ==# 'V'
let l:lines = getline("'<", "'>")
else
let l:lines = [getline('.')]
endif
else
" Operator-pending mode
let [l:lnum1, l:lnum2] = [line("'["), line("']")]
let l:lines = getline(l:lnum1, l:lnum2)
endif
" Find the most recent terminal buffer
let l:term_buf = -1
for l:buf in range(1, bufnr('$'))
if getbufvar(l:buf, '&buftype') ==# 'terminal'
let l:term_buf = l:buf
endif
endfor
if l:term_buf ==# -1
echoerr "No terminal buffer found"
return
endif
let l:ch = getbufvar(l:term_buf, 'terminal_job_id')
if exists('g:loaded_vim_terminal') && has('nvim')
" Neovim uses jobs API
for l:line in l:lines
call chansend(l:ch, l:line . "\n")
endfor
else
" Vim 8.1+ uses term_sendkeys
for l:line in l:lines
call term_sendkeys(l:term_buf, l:line . "\n")
endfor
endif
endfunction
nnoremap <leader>s :call SendToTerminal()<CR>
xnoremap <leader>s :<C-u>call SendToTerminal()<CR>
With these mappings, pressing <leader>s in normal mode sends the current line, and in visual mode it sends the selected lines to your REPL or shell.
Managing Terminal Buffers
Terminal buffers have buftype=terminal, which prevents Vim from writing them to disk. They also have a few quirks worth knowing:
- When the underlying program exits, the buffer remains but becomes read-only.
- You can re-enter terminal mode in an exited buffer with
iora, but only if the job is still running. - Use
:bdelete!to force-close a terminal buffer.
A helpful setting hides terminal buffers from the buffer list once the job exits, keeping :ls clean:
autocmd TermClose * set buflisted=false
You may also want to automatically enter terminal mode when a terminal buffer is opened:
autocmd BufWinEnter,WinEnter term://* startinsert
Running Tests and Builds
A common pattern is to dedicate one split to a terminal that runs your test suite or build watcher. You can open it once and re-run commands manually, or script it. Here's a mapping that runs the current file's tests in a bottom split:
function! RunTests() abort
let l:file = expand('%:p')
let l:cmd = 'pytest ' . shellescape(l:file)
" Reuse an existing terminal window if present
let l:win = bufwinnr('term://*pytest*')
if l:win !=# -1
execute l:win . 'wincmd w'
call term_sendkeys(bufnr('%'), l:cmd . "\n")
else
belowright 15split | call term_start(l:cmd, {'term_name': 'pytest'})
endif
endfunction
nnoremap <leader>t :call RunTests()<CR>
For Neovim, the equivalent uses jobstart and chansend:
function! RunTestsNVim() abort
let l:file = expand('%:p')
let l:cmd = ['pytest', l:file]
" Look for an existing terminal buffer named 'pytest'
for l:buf in nvim_list_bufs()
if nvim_buf_is_valid(l:buf) && getbufvar(l:buf, 'term_name', '') ==# 'pytest'
let l:chan = getbufvar(l:buf, 'terminal_job_id')
call chansend(l:chan, "pytest " . shellescape(l:file) . "\n")
return
endif
endfor
belowright 15split
call termopen(l:cmd, {'on_exit': {j,d,e -> setbufvar(bufname('pytest'), 'term_name', 'pytest')}})
endfunction
nnoremap <leader>t :call RunTestsNVim()<CR>
Using External Plugins
While the built-in terminal is capable, several plugins enhance the experience significantly:
- vim-floaterm: Floating terminal windows that can be toggled with a single key.
- toggleterm.nvim (Neovim): Persistent, toggleable terminals with lazygit and lazydocker integrations.
- vim-terminal-help: Sensible defaults for terminal mode navigation and persistence.
- send-to-terminal plugins (e.g., vim-slime): Mature solutions for REPL-driven development with tmux or terminal targets.
For example, with vim-slime, you configure a target terminal and then use <C-c><C-c> to send the current paragraph or selection:
let g:slime_target = "vimterminal"
let g:slime_vimterminal_cmd = "python3"
xmap <C-c><C-c> <Plug>SlimeRegionSend
nmap <C-c><C-c> <Plug>SlimeParagraphSend
Best Practices
- Keep one terminal per purpose: Separate terminals for builds, REPLs, and general shell work prevents confusion.
- Use descriptive names: Pass
term_name(Neovim) or useterm_startoptions (Vim) so you can identify buffers in:ls. - Map escape consistently: Pick one escape mapping and use it everywhere; muscle memory is everything.
- Avoid running long jobs in foreground terminals: Use
&or background jobs so Vim stays responsive. - Don't over-rely on terminal mode for editing: Use it for shells and REPLs; keep code editing in normal buffers.
- Learn
term_sendkeysandchansend: These are the building blocks for any custom automation. - Set
scrollbackappropriately: Long-running logs benefit from a larger scrollback; setset scrollback=10000in Neovim or useterm_startoptions in Vim. - Close stale terminals: Use
TermCloseautocommands to clean up buffers after jobs exit.
Common Pitfalls
Even experienced users hit a few recurring issues. Being aware of them saves hours of frustration:
- Stuck in terminal mode: If
Escdoesn't work, you likely haven't mapped it. UseCtrl-\ Ctrl-nas a fallback. - ANSY colors look wrong: Ensure
:set termguicolorsis enabled and your shell supports 256-color or true-color output. - Terminal buffer disappears: Some autocommands close splits on
BufLeave; review your config if terminals vanish. - Slow rendering: Heavy output (like
caton a huge file) can lag. Pipe throughlessor limit output. - Job still running after buffer close: In Vim, closing a terminal buffer may not kill the job. Use
term_killorjobstopin Neovim.
A Minimal Recommended Configuration
Putting it all together, here's a compact configuration that works in both Vim 8.1+ and Neovim:
" Terminal integration defaults
if has('terminal') || has('nvim')
" Escape terminal mode easily
tnoremap <Esc> <C-\><C-n>
tnoremap <C-h> <C-\><C-n><C-w>h
tnoremap <C-j> <C-\><C-n><C-w>j
tnoremap <C-k> <C-\><C-n><C-w>k
tnoremap <C-l> <C-\><C-n><C-w>l
" Open terminals conveniently
nnoremap <leader>tt :belowright 15split term://$SHELL<CR>
nnoremap <leader>tv :vertical split term://$SHELL<CR>
" Auto-enter terminal mode
autocmd BufWinEnter,WinEnter term://* startinsert
" Clean up on close
autocmd TermClose * set buflisted=false
endif
Drop this into your ~/.vimrc or init.vim, restart Vim, and you'll have a solid terminal workflow out of the box.
Conclusion
Vim's built-in terminal integration transforms the editor from a single-purpose tool into a self-contained development environment. By running shells, REPLs, and build tools inside Vim buffers, you preserve context, reduce context switching, and gain scriptable control over your entire workflow. Start with the basic :terminal command, add sensible escape and navigation mappings, then layer in custom functions for sending code and running tests. With a little configuration and a few best practices, terminal integration becomes an indispensable part of a fast, modern Vim setup — one that keeps you in the editor and in flow.