lib.nvim · Foundation · vimdoc
:help lib.nvim-kit
Themed, composable UI toolkit (lib.nvim.ui.kit)
doc/lib.nvim-kit.txt — rendered from the plugin's own vimdoc
*lib.nvim-kit.txt* Themed, composable UI toolkit (lib.nvim.ui.kit) lib.nvim.ui.kit *lib.nvim-kit*
CONTENTS
1. Introduction ......................... |lib.nvim-kit-intro| 2. Themes & presets ..................... |lib.nvim-kit-theme| 3. Surface .............................. |lib.nvim-kit-surface| 4. Components ........................... |lib.nvim-kit-components| 5. Status / roadmap ..................... |lib.nvim-kit-status|
1. INTRODUCTION
lib.nvim.ui.kit is a themed, composable UI toolkit. Pick a preset once and every
popup is visually coordinated, or override colors/borders per call. It is built
in layers on top of |lib.nvim-window| (make_scratch, nice_quit) and
lib.nvim.ui.hl; nothing shells out, so it is cross-platform.
User guide (with layout sketches): docs/GUIDE-ui-kit.md
Live theme playground:
:KitPreview
Opens a new tab split in two — a config buffer on the left and a gallery on the
right that re-renders with your theme as you type. <Tab> cycles the built-in
presets; <S-Tab> cycles installed nvim colorschemes (applied live, restored on
close). Also kit.preview().
2. THEMES & PRESETS
A theme is a token table (border, padding, zindex, title_pos, dims, hl). Built-in presets differ mainly in border strength:
minimal no border
rounded rounded border (default)
solid single border
double double border
ascii ASCII border glyphs (terminals without good Unicode)
menu rounded, coloured frame (Function) -- kit.menu's default
Highlights link to standard groups (NormalFloat / FloatBorder / FloatTitle / PmenuSel / …) so the default look is correct in any colorscheme. Register presets / choose the active default:
require("lib.nvim.ui.kit").setup({
default = "rounded",
presets = {
myproject = { border = "double", hl = { title = "Title" } },
},
})
A theme argument (anywhere one is accepted) is a preset name, a partial override table (deep-merged over the active default), or nil.
3. SURFACE
A surface is one themed float plus a lifecycle handle.
local kit = require("lib.nvim.ui.kit")
local s = kit.surface.open({ lines = { "hi" }, theme = "double", title = "X" })
s:set_lines({ "new", "content" })
s:set_title("Y")
s:focus()
s:on_close(function() end)
s:close()
open(opts) accepts: lines, theme, title, title_pos, width, height, relative,
row, col, zindex, enter, focusable, nice_quit, filetype, modifiable, wo, bo. It
returns the handle, or nil on failure.
4. COMPONENTS
kit.popup(opts)dispatches onopts.type. Implemented: note, viewer, toast, input, live_input, form, select, prompt, confirm, menu, picker, compare, progress.
kit.popup({ type = "note", title = "Saved", message = "Wrote 3 files", timeout = 2000 })
kit.popup({ type = "viewer", title = "Node Info",
lines = { "name: foo.lua", "size: 128 B" } })
kit.popup({ type = "toast", message = "background job done" })
kit.popup({ type = "input", prompt = "New name", default = "x",
on_submit = function(text) end })
kit.popup({ type = "input", prompt = "Password", secret = true,
on_submit = function(pw) end })
kit.popup({ type = "input", prompt = "Path", completion = "file",
on_submit = function(path) end })
kit.popup({ type = "live_input", prompt = "Filter",
on_change = function(query) end })
kit.popup({ type = "form", fields = {
{ name = "image", label = "Image", required = true },
{ name = "name", label = "Name" },
}, on_submit = function(values) end })
kit.popup({ type = "select", message = "Pick", selection = { "a", "b" },
on_select = function(choice, idx) end })
kit.popup({ type = "prompt", question = "Delete?", answer_type = "confirm",
on_answer = function(yes) end })
-- convenience aliases: kit.note / kit.viewer / kit.toast / kit.input /
-- kit.live_input / kit.form / kit.select / kit.prompt / kit.confirm /
-- kit.menu / kit.picker / kit.compare / kit.progress
note centered title + message float; optionaltimeout(ms) auto-dismiss. viewer read-only info panel; auto-sized to content; q/<Esc> closes, and (unlike note) closes the moment focus leaves it too. No timeout —opts.close_on_focus_lost = falseopts out of the focus-loss close. toast ephemeral top-right message; stacks; never steals focus; auto-dismiss. input single-line insert-mode prompt; <CR> submits, <Esc> cancels.secret = truemasks it as you type (a vim.fn.inputsecret replacement);maskoverrides the placeholder char (default "*").completion = "file"(or any getcompletion() type) wires <Tab> to the native completion popup (a vim.fn.input completion="file" replacement); <Tab>/<S-Tab> cycle it once open, <CR> accepts the highlighted candidate instead of submitting. live_input like input, but also debounces keystrokes intoon_change(query)as the user types (debouncems, default 80) — for filter/search boxes that refresh a results list or preview on every keystroke. form sequential multi-field prompt: chainedinputs collected into one keyed result table (`{ fields = { { name, label, default?, required? }, ... }, on_submit(values), on_cancel? }`). <Esc> skips an optional field (keeps its default); <Esc> on arequiredfield aborts the whole form and fireson_cancelinstead. select native themed list chooser (single/multi-select). This absorbed the former lib.nvim.ui.hover_select module, which has been removed. prompt ask:answer_type = "confirm"(yes/no, boolean answer) or"text". Addlayout = "buttons"for the horizontal button dialog. confirm button dialog: horizontal buttons, h/l/arrows move focus, <CR> confirms, <Esc>/q cancels, left click confirms a button directly (needs'mouse'=a; click on blank space is a no-op, not a cancel). Default Yes/No -> boolean; customchoices-> chosen string (cancel -> nil). menu cursor-anchored action list: items = { { label, action }, … }; picking an item runs its callback. Rows are padded a column at each edge and dividers are indented, so nothing sits flush against the border. Unlike the bare chooser it hides the block cursor while open, picks on a single left click, and dismisses on a click or focus change elsewhere -- turn any of the three off with hide_cursor / single_click / close_on_focus_lost. Defaults to themenupreset, so the frame is coloured rather than the quiet FloatBorder. Drilling into a submenu and walking back swap the list inside the SAME window, so neither flashes and the menu does not move. progress passthrough to |lib.nvim-progress| (lib.nvim.progress.create); returns its handle (:update / :finish / :cancel). compare pick two items out of one picker, view them side by side: SEARCH (prompt + results + live preview) -> MARKED (mark_key, default <M-c>, or <CR>, freezes the first pick while search continues) -> COMPARE (<CR> again: two full-height preview panes). `{ items, render(item, surface), format_item?, query?, on_compare?(a, b), on_close?(a, b) }`. Engine-independent: the caller'srenderpaints each item (buffer text, a diff, images.nvim's terminal-drawn images, …), so nothing here is content-specific.kit.sync(open_fn, opts, timeout_ms?)blocks (viavim.wait) until an on_submit/on_cancel-shaped async component (input/form/live_input) resolves, returning its result as a plain value instead of via callback — for call chains built around a blockingvim.fn.input()that can't easily be recast to callback style. Returnsresult, cancelled, timed_out. Only safe to call from a normal call stack, never a fast-event context.
5. STATUS / ROADMAP
Phase 1: theme/preset engine,surfaceprimitive,note. Phase 2:toast,input,select,prompt. Phase 3: layout engine (kit.layout) + template registry + nativeselectchooser (which absorbed and replaced the removed hover_select) +kit.picker. Phase 4: button-confirm (kit.confirm/promptwithlayout = "buttons"). Phase 5:kit.viewer, a read-only info panel — the "show some info, dismiss it" float that was hand-rolled independently across consumer plugins (6+ times in filetree.nvim alone). Phase 6:kit.form, a sequential multi-field prompt — chainedkit.inputcalls collected into one keyed result table, replacing the hand-rolled "several vim.fn.input calls in a row" pattern (sandbox.nvim's container_commands.lua, buffer_ctx.nvim's own process_prompts helper). Phase 7:kit.live_input,kit.inputplus a debouncedon_change(query)— replacing the hand-rolled floating prompt-buffer + TextChangedI-debounce pattern duplicated in filetree.nvim's live_search and filter features. Phase 8:kit.sync, avim.waitbridge that blocks on an async input/form/live_input and returns its result as a plain value, for call chains built around a blocking vim.fn.input() that can't easily be recast to callback style. Phase 9: richkit.selectitems — multi-line entries with per-span custom highlights, navigation by logical item instead of raw line. Phase 10:kit.input({ secret = true })— masked entry, a vim.fn.inputsecret replacement. Each character is concealed behindmask(default "*"), re-derived from the buffer on every edit; the real text still reaches on_submit. Phase 11:kit.input({ completion = "file" })— file-path (or any getcompletion() type) completion via <Tab>, a vim.fn.input completion="file" replacement built on real ins-completion (vim.fn.complete()), not a custom picker. Phase 12:kit.compare— pick two items out of one picker, view them side by side (SEARCH -> MARKED -> COMPARE), motivated by images.nvim's "browse, pick two, compare" roadmap item but content-agnostic by construction. Phase 13 (this release):kit.sync, thevim.waitbridge described above — for call chains built around a blocking vim.fn.input() that can't easily be recast to callback style. The layout engine turns a declarative region spec into aligned nvim_open_win geometry for several coordinated floats:
local group = require("lib.nvim.ui.kit").layout.template("picker", { theme = "rounded" })
group.slots.results:set_lines(matches)
group.slots.preview:set_lines(preview)
-- group.close() closes every slot
kit.layout.compute(spec)returns the pure geometry (unit-testable);kit.layout.mount(spec, opts)opens themed surfaces into the slots.kit.picker(opts)makes the picker template interactive (Telescope-style): an insert-mode prompt debounces intoon_change(query)(the caller fills the results slot viahandle.set_results(lines)), <C-n>/<C-p>/arrows move the selection, <CR> callson_submit(idx, text), <Esc> closes. Passprompt = "plain"for a bare template you wire yourself.