https://github.com/user-attachments/assets/fe957e65-6e7d-4862-a47c-f618202a42fb
Turn *.term files into persistent terminal sessions.
Create a file called watcher.term, open it, and instead of seeing the file's
contents you get a terminal. Because the terminal is a real buffer backed by a
real file, it shows up in file pickers, :buffers, sessions, and anything else
that works with files. When you quit Neovim the shell is closed (a live process
can't literally be frozen), but the terminal's raw output is saved into the
.term file. Reopening replays that output — with its original colors — and
drops you into a fresh shell in the same working directory, so the session looks
and feels like you never left.
No commands. No manual invocation. It just works via autocommands.
A .term file is not a script or a log — it's the raw byte stream the
terminal produced (escape sequences, colors, and all). Reopening the file feeds
that stream back into a terminal, which re-renders it natively. Two facts are
worth being clear about:
.term file → get a terminal. The raw recording is never shown as
text; the buffer becomes a live terminal instead.watcher.term, so Telescope /
fzf-lua / :find / grep all treat it normally.TermOpen, so terminal plugins attach to it (see below).on_stdout tap to record the raw stream — no script, no daemon.jobstart({ term = true }))/proc (Linux). On other platforms cwd is
restored only if your shell itself emits OSC 7; otherwise the new shell starts
in the .term file's directory. Everything else works everywhere.With lazy.nvim:
{ "TheLazyCat00/termfile-nvim" }
With packer.nvim:
use "TheLazyCat00/termfile-nvim"
That's it — no config/setup call is required.
:e watcher.term
or, from your shell:
nvim watcher.term
A terminal opens. Use it like any terminal. Quit Neovim whenever you like — the
next time you open watcher.term, its output is replayed (in color) and you're
back in a shell in the same working directory.
Create as many as you want (build.term, logs.term, server.term, …). Each
is an independent, revivable session that your file picker can jump to.
Configuration is entirely optional. To override a default, call setup:
require("termfile").setup({
-- Shell to launch. nil = Neovim's 'shell'. String or list.
shell = nil,
-- Replay the recorded output when reopening a .term file.
restore = true,
-- Inserted between restored output and the new shell prompt.
-- Use "" to append the prompt directly, as in earlier versions.
delimiter = "\n",
-- Max bytes of recorded output kept per file (older output trimmed).
max_bytes = 256 * 1024,
-- Enter terminal insert-mode automatically when a .term buffer is focused.
start_insert = false,
-- Restore number/relativenumber from Neovim's global option defaults after
-- a .term buffer leaves a window. This follows values set with vim.opt.
default_win_opts = "global",
-- Alternatively, apply explicit window-local values:
-- default_win_opts = { number = true, relativenumber = true },
-- Persist output on unload / write / exit.
auto_save = true,
-- Glob identifying terminal-session files.
pattern = "*.term",
})
Because replay reproduces the raw stream literally, a session that ended inside
a full-screen TUI (vim, htop, …) or right after clear will replay those
control sequences too — so its restored "history" may look sparse or blank. For
ordinary shell work (builds, logs, git) it round-trips faithfully, colors and
all.
Because a .term buffer is a real built-in terminal, plugins that hook
TermOpen and drive the shell through vim.bo.channel work with it
unchanged. For example, editable-term.nvim
(Neovim motions inside terminals) attaches automatically — no extra
configuration on either side.
Snacks picker normally excludes buffers
whose buftype is terminal when it chooses the main window for an opened
file. Consequently, invoking a picker while focused in a terminal can make the
file open in another eligible window or in a new split instead of replacing the
terminal.
termfile.nvim marks every managed buffer with b:snacks_main = true. Snacks
checks this explicit preference before its terminal-buffer filter, so a file
selected while a .term window is focused can replace that terminal normally.
The marker is harmless when Snacks is not installed and requires no user
configuration.
Because replay reproduces the raw stream literally, a session that ended inside
a full-screen TUI (vim, htop, …) or right after clear will replay those
control sequences too — so its restored "history" may look sparse or blank. For
ordinary shell work (builds, logs, git) it round-trips faithfully, colors and
all.
BufReadCmd / BufNewFile autocommand on *.term takes over loading the
file, then starts a real built-in terminal in the buffer with
jobstart(shell, { term = true, on_stdout = … }).term = true renders the output and, because an on_stdout callback is
set, hands us the same raw bytes — so we record the exact stream (colors and
all) while Neovim draws it. On reopen the recording is cat-ed into the
terminal before the shell starts, so previous output is re-rendered (and
re-recorded) without being fed to the shell as input.term://… to its .term path so its
filename is preserved.BufUnload, BufWriteCmd, and VimLeavePre write the recording (capped to
max_bytes) back to the file, appending an OSC 7 sequence with the shell's
current directory.