doc/lib.nvim-window.txt — rendered from the plugin's own vimdoc
*lib.nvim-window.txt* Helpers for overlay / floating windows lib.nvim.window *lib.nvim-window* Small, focused helpers for overlay and floating windows — hover popups, pickers, debug panels, transient info floats. Instead of re-implementing the same boilerplate (scratch buffers, close-on-key, titles, positioning) in every plugin, call one helper and pass the window id.
CONTENTS
1. Design ..................................... |lib.nvim-window-design| 2. Usage ...................................... |lib.nvim-window-usage| 3. Functions .................................. |lib.nvim-window-functions| make_scratch ............................ |lib.nvim-window-make_scratch| nice_quit ............................... |lib.nvim-window-nice_quit| set_title ............................... |lib.nvim-window-set_title| close_on_focus_lost ..................... |lib.nvim-window-close_on_focus_lost| center .................................. |lib.nvim-window-center| open_scratch_split ...................... |lib.nvim-window-open_scratch_split| open_named_scratch ...................... |lib.nvim-window-open_named_scratch| tag ...................................... |lib.nvim-window-tag| is_usable_window/target_window .......... |lib.nvim-window-find_usable| ensure_bottom/make_focusable/force_focus/ reveal_at_bottom ....................... |lib.nvim-window-focus_helpers| attach .................................. |lib.nvim-window-attach|
1. DESIGN
* One responsibility per function — small, independently testable building blocks. * Idempotent and defensive — an invalid window id is a safe no-op (guarded bypcall/nvim_win_is_valid), never a crash. * Buffer-local — keymaps and autocommands only affect the target window. * Composable — make_scratch builds on nice_quit; nothing is implemented twice.
2. USAGE
Free functions (recommended in plugin code; tree-shake friendly):
local window = require("lib.nvim.window")
local winid, bufnr = window.make_scratch({ lines = { "hi" }, title = "Info" })
window.nice_quit(winid)
window.center(winid)
Constructor (attach) — a handle bound to one window id, called with DOT
syntax (no implicit self):
local w = require("lib.nvim.window").attach(winid)
w.nice_quit()
w.set_title("New Title")
w.center()
attach is pure sugar: every method delegates to the matching free function
with winid pre-applied. The free functions remain the single source of
truth.
Individual functions can also be required directly:
local make_scratch = require("lib.nvim.window.make_scratch")
3. FUNCTIONS
make_scratch({opts})
Create an unlisted scratch buffer (nofile,bufhidden=wipe, no swapfile) inside a centered float. Returnswinid, bufnr, ornil, nilon failure (the buffer is cleaned up again in that case).
local winid, bufnr = window.make_scratch({
lines = { "line 1", "line 2" },
title = "Hover",
nice_quit = true, -- q / <Esc> close immediately
filetype = "markdown",
})
Options
lines string[] initial content (default {})
width integer default sizes to content, clamped to editor
height integer default is line count, clamped to editor
relative string "editor" | "cursor" | "win" (default "editor")
row, col integer explicit position; default centers on editor
border string|string[] border style (default "rounded")
title string float title (only shown with a border)
title_pos string "left" | "center" | "right"
focusable boolean whether the float is focusable (default true)
enter boolean focus the new window on creation (default true)
zindex integer stacking order
filetype string buffer filetype
modifiable boolean keep buffer writable (default false, read-only)
nice_quit boolean|table attach q / <Esc> close behaviour
wo table window-local option overrides
bo table buffer-local option overrides
Overlay window defaults (number=false, relativenumber=false, signcolumn=no,
wrap=false, cursorline=false, style=minimal) can be overridden via opts.wo.
Content is set first, then the buffer is locked to nomodifiable (unless
modifiable = true).
nice_quit({winid}, {opts})
Bindqand<Esc>(buffer-local, NORMAL mode only) to close the window. Returns true when the keymaps were attached.
window.nice_quit(winid)
window.nice_quit(winid, { keys = { "q" }, force = true })
Options
keys string[] normal-mode keys to close (default { "q", "<Esc>" })
force boolean discard unsaved changes (default false)
Why normal mode only: this gives the natural "double Escape" for free. The
first <Esc> leaves Insert/Terminal mode (Vim default), the second <Esc> — now
in Normal mode — closes the window. Insert/Terminal mode is never mapped, so
TUI programs (fzf, lazygit, …) keep Escape for themselves. The maps use
nowait, so there is no timeoutlen delay. The last window in the tabpage is
never closed.
set_title({winid}, {title}, {opts})
Set (or clear, with nil) the title of a floating window. A safe no-op on any
non-floating window.
window.set_title(winid, "New Title", { pos = "center" })
window.set_title(winid, nil) -- clear
Options
pos string title_pos: "left" | "center" | "right" Note: Neovim only stores and renders a float title when the float has a border. Without a border the title has no effect (a debug notice is emitted).
close_on_focus_lost({winid}, {opts})
Register a one-shot, buffer-local autocommand that closes the window as soon as
focus leaves it — the usual hover/popup dismiss. Returns the augroup id, which
can be cancelled again via nvim_del_augroup_by_id.
local grp = window.close_on_focus_lost(winid)
vim.api.nvim_del_augroup_by_id(grp) -- cancel
Options
events string[] events counting as focus loss
(default { "WinLeave", "BufLeave" })
force boolean discard unsaved changes (default true)
The autocommand is once = true (self-cleaning) and closes via vim.schedule,
since closing directly from inside WinLeave would be unsafe.
center({winid})
Recenter an existing float on the editor, using its current width/height and the editor size. A safe no-op on non-floats and invalid ids.
window.center(winid)
open_scratch_split({lines}, {opts})
Open a fresh scratch buffer (nofile,bufhidden=wipe, no swapfile) in a plain split. Every call opens its own window — unlike |lib.nvim-window-open_named_scratch|, there is no de-duplication by buffer name, which is the right behaviour for report/audit-style output where a second invocation should produce its own buffer. Returnsbufnr, winid.
local bufnr, winid = window.open_scratch_split(report_lines, {
filetype = "my-plugin-report",
})
Options
filetype string buffer filetype
split string "above" | "below" | "left" | "right";
unset honors 'splitbelow'/'splitright'
modifiable boolean keep buffer writable (default false, read-only)
open_named_scratch({name}, {lines}, {opts})
Find-or-create a named scratch buffer (nofile,bufhidden=hide, no swapfile) shown in a split. Complementsmake_scratch(always a float) andopen_scratch_split(always a new window): identified by a stable buffername, so a second call reuses the same buffer/window — for a log viewer or list view — instead of piling up duplicates. Returnsbufnr, winid.
local bufnr, winid = window.open_named_scratch("MyPlugin://log", lines, {
filetype = "my-plugin-log",
split = "below",
})
If a window already shows the buffer (in any tab), that window is focused
and its content replaced rather than opening a second one; otherwise a new
split is opened per opts.split.
Options
filetype string buffer filetype
split string "above" | "below" | "left" | "right"
(default "below")
size integer window height (or width, for "left"/"right")
modifiable boolean keep buffer writable (default false, read-only)
tag
Small namespace for identifying a window later by an arbitrary string tag, without keeping your own registry (a registry can go stale when a window closes through a path you never observed). Uses the samevim.w[win].custom_tagconvention aslib.nvim.buf_win_tab.capture'stagoption, so windows tagged by either can be found by the other.
window.tag.set(winid, "my-plugin://report")
local found = window.tag.find("my-plugin://report") -- nil if not open
local t = window.tag.get(winid) -- read it back
tag.find only matches live, real content windows (not hidden or degenerate
floats with width/height <= 1).
is_usable_window({winid}), target_window({opts})
is_usable_window(winid)is true for a normal, non-floating window that isn't a well-known sidebar (neo-tree, NvimTree, aerial, Outline, qf by filetype) — for a plugin that wants to reuse an existing normal split instead of always opening a new one.target_window(opts)returns the first such window, preferring the current one, or nil if none qualifies.
local target = window.target_window({ current_tab_only = true })
if target then vim.api.nvim_win_set_buf(target, bufnr) end
Options (`target_window`)
current_tab_only boolean search only the current tabpage's windows
(default false: every window)
ensure_bottom({winid}), make_focusable({winid}),
force_focus({winid}), reveal_at_bottom({winid})
*lib.nvim-window-focus_helpers*
Small helpers for log/output-style windows. ensure_bottom moves the
cursor to the last line, retrying on the next tick if the window isn't
valid yet (e.g. right after creation). make_focusable flips a floating
window created with focusable = false to focusable; a no-op returning
false on a non-floating window. force_focus makes the window focusable
first, then focuses it. reveal_at_bottom does both: focus, then scroll
to bottom. All but ensure_bottom return ok (boolean).
local h = window.make_scratch({ lines = {}, focusable = false })
window.reveal_at_bottom(h) -- streamed output: focus + follow the tail
attach({winid})
Construct a handle bound towinid. Every function above that takeswinidas its first argument is available as a method (dot syntax).
local w = window.attach(winid)
w.set_title("Title")
w.nice_quit()
w.center()
w.close_on_focus_lost()