Problem: Wrong terminal scrollback if BufFile* autocommand drains PTY
output but doesn't process the pending refresh.
Solution: Refresh scrollback before refreshing screen in terminal_open()
if scrollback has been allocated.
Problem: Terminal loses output if a BufFilePre or BufFilePost autocmd
polls for events.
Solution: Rename the buffer after allocating the terminal instance. Also
fix buffer getting wrong name if BufFilePre uses NameBuff.
Usually, terminal_close() calls refresh_terminal(), which allocates the
scrollback buffer, and term_may_alloc_scrollback() in terminal_open()
won't dereference the buffer. However, refresh_terminal() is not called
during Nvim exit, in which case a heap-use-after-free may happen if
TermOpen wipes buffer. Check for non-NULL buf_handle to avoid that.
Problem: When TermOpen polls for enough events to use the scrollback
buffer, scrollback is lost until the next terminal refresh.
Solution: Allocate the scrollback buffer when it's needed.
Problem: :edit and :enew may reuse a 1-line terminal buffer, causing
the new buffer to still be a terminal buffer.
Solution: Don't reuse a terminal buffer, as it's not reused when it has
more than 1 line.
After this change close_buffer() is the only place where buf_freeall()
can be called on a terminal buffer, so move the buf_close_terminal()
call into buf_freeall() to save some code. Furthermore, closing the
terminal in buf_freeall() is probably more correct anyway, as it is
"things allocated for a buffer that are related to the file".
Also, remove the useless check for on_detach callbacks deleting buffer.
Even if b_locked fails to prevent that, the crash will happen at the end
of buf_updates_unload() first. On the other hand, many other call sites
of buf_updates_unload() and other buffer_updates_* functions don't set
b_locked, which may be a problem as well...
Problem: Calling termopen() or nvim_open_term() on a buffer with an
existing terminal leads to two terminals writing to the same
buffer if the terminal job is still running, or memory leak
if the terminal job has exited.
Solution: Close the terminal if the terminal job has exited, otherwise
report an error. For nvim_open_term() also don't write a
closed terminal's buffer content to the PTY.
Problem: Crash when a terminal receives OSC 2 just after closing its
buffer but before terminal job exits.
Solution: Remove FUNC_ATTR_NONNULL_ALL from buf_set_term_title() and
check for NULL.
Problem: Destroying a terminal with pending TermRequest leads to
heap-use-after-free when processing TermRequest afterwards.
Solution: Store the buffer handle instead of the Terminal pointer in the
pending TermRequest event, and don't emit TermRequest if the
terminal is already closed.
Problem: When switching to another terminal buffer in Terminal mode,
usually Nvim stays in Terminal mode, but leaves Terminal mode
if the old terminal buffer was deleted.
Solution: Don't always leave Terminal mode when active terminal buffer
is deleted. Instead let terminal_check_focus() decide that.
Problem:
Terminals should respond with the terminator (either BEL or ST) used in
the query so that clients can reliably parse the responses. The
`TermRequest` autocmd used to handle background color requests in the
terminal does not have access to the original sequence terminator, so it
always uses BEL. #37018
Solution:
Update vterm parsing to include the terminator type, then forward this
data into the emitted `TermRequest` events for OSC/DCS/APC sequences.
Update the foreground/background `TermRequest` callback to use the same
terminator as the original request.
Details:
I didn't add the terminator to the `TermResponse` event. However, I
assume the `TermResponse` event doesn't care about the terminator
because the sequence is already parsed. I also didn't update any of the
functions in `src/nvim/vterm/state.c` that write out responses. It
looked like those all pretty much used ST, and it would be a much larger
set of changes. In that same file, there's also logic for 8 bit ST
sequences, but from what I can tell, 8 bit doesn't really work (see `:h
xterm-8bit`), so I didn't use the 8 bit ST at all.
Problem:
Every CI log has a lot of noise at the end, which makes it harder to
find relevant test failures:
Running tests from test/functional/terminal/tui_spec.lua
...
T5831 TUI bg color queries the terminal for background color:
T5832 TUI bg color triggers OptionSet from automatic background processing:
T5833 TUI bg color sends theme update notifications when background changes #31652:
...
Running tests from test/functional/ui/output_spec.lua
...
WRN 2025-12-02T03:36:47.304 ui/c/T5831.28003.0 tui_handle_term_mode:223: TUI: terminal mode 2026 unavailable, state 0
WRN 2025-12-02T03:36:47.359 ui/c/T5832.28006.0 tui_handle_term_mode:223: TUI: terminal mode 2048 unavailable, state 0
WRN 2025-12-02T03:36:47.414 ui/c/T5833.28009.0 tui_handle_term_mode:223: TUI: terminal mode 2048 unavailable, state 0
Solution:
- Skip logging in test-mode.
- This can be reverted later, when these logs are changed to "INFO"
level, per this TODO comment:
```
// TODO(bfredl): This is really ILOG but we want it in all builds.
// add to show_verbose_terminfo() without being too racy ????
WLOG("TUI: terminal mode %d unavailable, state %d", mode, state);
```
Problem:
Test sometimes fails on macos:
test/functional/terminal/channel_spec.lua:96: Expected objects to be the same.
Passed in:
(number) 3
Expected:
(number) 2
Solution:
Retry a few times.
compile time features are hot again.
Note: this changes the &term value for builtin definition from
'builtin_xterm' to just 'xterm'. It's an xterm regardless of we use an
external definition or an internal. Prior to this commit the vast
majority of POSIX users will have used external terminfo, so plugins and
scripts are only going to have checked for &term == 'xterm' or 'tmux' or
whatever.
The status of external loading is still available in "nvim -V3" output.
:restart changes the terminal under which the server process is run,
which can be confusing when combined with other testing.
Also, use COLORTERM=xterm-256color for tests with 'notermguicolors' to
avoid delay on server exit. Not all these delays are blocking, but they
increase the number of simultaneously existing Nvim processes.
The processing of terminfo can be separated into two steps:
1. The initialization of terminfo, which includes trying to find $TERM
in a terminfo database file. As a fallback, common terminfo
definitions are compiled in. After this, we apply a lot of ad-hoc
patching to cover over limitations of terminfo.
2. While processing updates from nvim, actually using terminfo strings
and formatting them with runtime values. for this part, terminfo
essentially is a hyper-enhanced version of snprintf(), including
a sm0l stack based virtual machine which can manipulate the runtime
parameters.
This PR completely replaces libuniblium for step 2, with code
vendored from NetBSD's libtermkey which has been adapted to use typesafe
input parameters and to write into an output buffer in place.
The most immedatiate effects is a performance enhancement of
update_attrs() which is a very hot function when profiling the
TUI-process part of screen updates. In a stupid microbenchmark
(essentially calling nvim__screenshot over and over in a loop) this
leads to a speedup of ca 1.5x for redrawing the screen on the TUI-side.
What this means in practise when using nvim as a text editor is probably
no noticible effect at all, and when reabusing nvim as idk a full screen
RGB ASCII art rendrer maybe an increase from 72 to 75 FPS LMAO.
As nice side-effect, reduce the usage of unibilium to initialization only..
which will make it easier to remove, replace or make unibilium optional,
adressing #31989. Specifically, the builtin fallback doesn't use
unibilium at all, so a unibilium-free build is in principle possible
if the builtin definitions are good enough.
As a caveat, this PR doesn't touch libtermkey at all, which still has a
conditional dependency on unibilium. This will be investigated in a
follow-up PR
Note: the check of $TERMCOLOR was moved from tui/tui.c to
_defaults.lua in d7651b27d5 as we want to
skip the logic in _defaults.lua if the env var was set, but there
is no harm in TUI getting the right value when the TUI is trying to
initialize its terminfo shenanigans. Also this check is needed when
a TUI connects to a `--headless` server later, which will observe
a different $TERMCOLOR value than the nvim core process itself.
Problem: w_wrow/col calculation in terminal_check_cursor is wrong when the
terminal is smaller than the window. Common when there's a larger window open
with the same terminal, or just after a resize (as refresh_size is deferred).
Solution: don't calculate it; validate_cursor will correct it later if it's
out-of-date.
Note that the toplines set for the terminal (also before this PR) assume
'nowrap' (which is set by default for terminal windows), and that no weird stuff
like filler lines are around. That means, for example, it's possible for the
cursor to be moved off-screen if there's wrapped lines. If this happens, it's
likely update_topline will move the cursor back on screen via validate_cursor or
similar, but maybe this should be handled more elegantly in the future?
Problem: autocommands can cause various problems in terminal mode, which can
lead to crashes, for example.
Solution: fix found issues. Move some checks to terminal_check and guard against
autocommands messing with things. Trigger TermEnter/Leave after terminal mode
has changed/restored most state. Wipeout the correct buffer if TermLeave
switches buffers and fix a UAF if it or WinScrolled/Resized frees the terminal
prematurely.
These changes also allow us to remove the buffer restrictions on TextChangedT;
they were inadequate in stopping some issues, and WinScrolled/Resized was
lacking them anyway.
Problem: when creating a new tabpage with a terminal window, terminal size is
not updated if there is no statusline.
Solution: do not rely on last_status to implicitly call terminal_check_size as a
side effect of making room for a statusline; call it directly.
Problem: topline of a focused terminal window may not tail to terminal output if
events scroll the window.
Solution: set the topline in terminal_check_cursor.