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 *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-kit-intro*

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 *lib.nvim-kit-theme*

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 *lib.nvim-kit-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 *lib.nvim-kit-components*

kit.popup(opts) dispatches on opts.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; optional timeout (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 = false opts 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 = true masks it as you type (a vim.fn.inputsecret
          replacement); mask overrides 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 into on_change(query)
          as the user types (debounce ms, default 80) — for filter/search
          boxes that refresh a results list or preview on every keystroke.
  form    sequential multi-field prompt: chained inputs 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 a required field
          aborts the whole form and fires on_cancel instead.
  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".
          Add layout = "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; custom choices -> 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 the menu preset, 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's render paints 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 (via vim.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 blocking vim.fn.input() that can't easily be recast
to callback style. Returns result, cancelled, timed_out. Only safe to call
from a normal call stack, never a fast-event context.

5. STATUS / ROADMAP *lib.nvim-kit-status*

Phase 1: theme/preset engine, surface primitive, note.
Phase 2: toast, input, select, prompt.
Phase 3: layout engine (kit.layout) + template registry + native select
chooser (which absorbed and replaced the removed hover_select) + kit.picker.
Phase 4: button-confirm (kit.confirm / prompt with layout = "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 — chained kit.input
calls 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.input plus a debounced on_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, a vim.wait bridge 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: rich kit.select items — 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 behind mask (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, the vim.wait bridge 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 into on_change(query) (the caller fills the
results slot via handle.set_results(lines)), <C-n>/<C-p>/arrows move the
selection, <CR> calls on_submit(idx, text), <Esc> closes. Pass
prompt = "plain" for a bare template you wire yourself.