btv.ui
btv.ui.select(items, opts, on_choice)
btv.ui.select(items, opts) -> a PROMISE that resolves to the chosen item, or to
nil on cancel (<Esc> / q). Promise-only: there is no on_choice argument (passing
one is the old callback shape and errors loudly). The 1-based index is dropped —
recover it from the item, or use the vim.ui.select alias, which keeps it.
opts:
prompt— the label drawn above the list (default none).format_item— anitem -> display stringmapper (defaulttostring); the original item round-trips to the resolved value regardless.
Defined in ui.lua.
btv.ui.menu(entries, opts)
btv.ui.menu(entries, opts) -> a PROMISE that resolves to the chosen entry, or to
nil when the menu is dismissed (<Esc>, q, a click off it). A context menu: the
same floating list as btv.ui.select, with rows that say more than a label. Each
entry is a table:
{
label = "Copy", -- the row's text (required unless `separator`)
detail = "Ctrl+C", -- shown in an aligned column on the right, e.g. a shortcut
enabled = false, -- greyed out; can't be chosen (default true)
separator = true, -- a rule between groups of rows; can't be chosen
}
Keys and the mouse skip the rows that can’t be chosen. Any other field rides
along on the entry the promise resolves to (an action to run, say).
opts:
anchor—"cursor"(default) opens the list under the text cursor;"mouse"opens it at the last mouse cell, the shape for aMenuPopuphandler (under the text cursor instead when a key came after that click). At the pointer it behaves like a GUI menu: one click chooses a row, the pointer hovering a row highlights it, and a click or right-click off it dismisses it.prompt— as forbtv.ui.select.
Greyed-out rows paint with BtvMenuDisabled (default: linked to Comment);
separators are drawn in the menu’s border color.
Defined in ui.lua.
btv.ui.input(opts, on_confirm)
btv.ui.input(opts) -> a PROMISE that resolves to the entered string on <CR>, or
to nil on <Esc> (cancel). Promise-only: there is no on_confirm argument (passing
one is the old callback shape and errors loudly).
opts:
prompt— the label drawn ahead of the editable line (default"").default— text prefilled into the line, cursor at its end (default"").history— a namespace string enabling readline-style recall:<Up>/<Down>(and<C-p>/<C-n>) browse the prompts submitted under this namespace, and each non-empty submission is recorded into it. Each namespace is an independent ring, so one plugin’s REPL history is separate from another’s. Session-only for now. Absent ⇒ no history.complete— a function(line, col) -> candidatesdriving<Tab>autocomplete (the inline wildmenu above the prompt line —<Tab>/<S-Tab>cycle,<CR>accepts).candidatesis a{ {label, insert?, doc?, start?, length?}, … }list (insertdefaults tolabel), OR a PROMISE of one — so an async source (e.g. a DAPcompletionsrequest) works. The token completed is the trailing identifier run before the cursor, UNLESS a candidate suppliesstart(0-based char offset into the line) +length(chars), an explicit replace span that overrides the token for that row.colis the cursor’s 0-based char offset.complete_docs— show the side docs pane rendering each candidate’sdocbeside the list (default true whencompleteis set;falsesuppresses it).complete_debounce— ms to coalesce refresh queries (narrowing an open menu as you type) so an async source isn’t a wire round-trip per keystroke; the initial<Tab>is always immediate (default 100;0disables it).
The server owns the prompt: it opens the editor’s command line as a labelled
Prompt (Editor::open_prompt), and delivers the result to btv._cb_fns[id] through
the shared prompt_results channel. Non-blocking (ADR 0002 rule 3): the call
returns at once and the promise settles on a later tick. Note an empty submission
(<CR> on an empty line) resolves to "" (not nil) — only <Esc> cancels, matching
neovim’s vim.ui.input.
Defined in ui.lua.
btv.ui.confirm(message, opts, on_choice)
btv.ui.confirm(message, opts) -> a PROMISE that resolves to a boolean — true on
Yes, false on No or cancel (<Esc>). Promise-only: there is no on_choice argument
(the old callback forms — a third arg, or opts-as-function — error loudly).
opts (optional):
default—true|false, which button<CR>selects (defaulttrue= Yes).
btv-native (no vim.ui twin): neovim spells this blocking vim.fn.confirm, which the
btv model omits (rule 3). For an arbitrary multi-choice menu use btv.ui.select
instead — confirm is deliberately just yes/no. The server opens a single-keypress
Confirm dialog (Editor::open_confirm) sharing the prompt_results channel with
btv.ui.input (one prompt open at a time); the chosen 1-based button index arrives
as a string, which the wrapper folds to the boolean (1 = Yes; 2 = No; 0 = cancel).
Defined in ui.lua.
float:update(contents, opts)
No documentation comment in the prelude.
Defined in ui.lua.
float:close()
No documentation comment in the prelude.
Defined in ui.lua.
float:is_open()
No documentation comment in the prelude.
Defined in ui.lua.
btv.ui.float(contents, opts)
btv.ui.float(contents, opts): open the list-less content float — the sibling of
the selectable-list widget (docs/specs/2026-06-14-btv-ui-float-widget.md, “What
stays out of this widget”) — rendering content with no list / selection.
contents is a string (split on newlines), a list of line strings, or — for a
styled float (the “pretty” which-key) — a list where a row may be a CHUNK LIST
{ {text, hl_group?}, … } (neovim’s virt_text shape): each chunk paints its
text in hl_group, so a row can colour its key one group and its description
another, or dim a whole row with a Comment/dim group. Plain and chunk rows mix
freely; a plain row is just one un-grouped chunk.
opts:
border—"none"|"single"|"rounded"|"double"|"solid"(default"rounded").title— a string drawn on the top border (optional).relative—"cursor"(default, anchors at the cursor) |"editor"(centered) |"bottom"(pinned to the editor’s bottom-right corner — the which-key shape).persist— when truthy, the float survives keystrokes (it is not dismissed by the next key) andbtv.ui.floatreturns a HANDLE with:update(contents, opts)/:close()/:is_open(). This is the surface a key-observer plugin (e.g. which-key) renders through, refreshing it as keys arrive. Withoutpersistit is fire-and-forget: the server owns the float, its geometry, and its dismissal (the next key closes it); returns nil. Empty contents open nothing. LSP hover and signature help use the non-persistent form.
Defined in ui.lua.
btv.ui.open(uri)
btv.ui.open(uri) -> a PROMISE of the opener’s exit result { code, stdout, stderr }
(the btv.run shape). Hands uri — a file path or a URL — to the OS opener chosen
by platform (btv._ui_opener: open on macOS, explorer on Windows, xdg-open
elsewhere) and runs it off-tick. Like btv.run it RESOLVES rather than rejects: a
missing opener surfaces as code = -1 and a non-zero opener exit as that code —
the caller decides what to do with it. Promise-only (ADR 0002): no callback arg.
Defined in ui.lua.
btv.ui.caps()
btv.ui.caps() -> a fresh table of the attached client’s terminal capabilities:
{
client = "", -- "tui", "gui" or "web"; "" when the client didn't say
platform = "", -- "macos", "linux", "windows"; "" when it didn't say
keyboard_protocol = false, -- the kitty keyboard protocol is on
truecolor = false, -- the terminal can show 24-bit color
osc52 = false, -- the terminal accepts OSC 52 clipboard writes
key_labels = {}, -- chords this client can only deliver via another chord
}
keyboard_protocol is the one a keymap cares about. Without it the terminal
cannot tell <C-i> / <C-m> / <C-[> / <C-h> apart from <Tab> / <CR> /
<Esc> / <BS>, and bemtvi folds each onto the named key — on BOTH sides, so
mapping <C-h> there really maps <BS>. A plugin that wants one of those four
chords should install it only when this is true:
btv.on("UIEnter", {}, function()
if btv.ui.caps().keyboard_protocol then
btv.keymap.set("i", "<C-h>", my_action)
end
end)
key_labels maps a chord the editor sees onto the chord the user must actually
PRESS to send it, for the clients that cannot deliver it directly. The browser is
the case that forced it: Chrome and Edge handle <C-w> / <C-t> / <C-n> /
<C-1>..<C-9> themselves on Windows and Linux, so the page never sees them and
the web client substitutes Alt — a keypress of Alt+w arrives as <C-w>. Anything
that DISPLAYS a key (a which-key popup, a cheat sheet, a desc listing) should
render through this so it names a chord the user can press; anything that MATCHES
on keys must not, since the editor still sees the canonical notation:
local labels = btv.ui.caps().key_labels
local shown = labels["<C-w>"] or "<C-w>" -- "<A-w>" in a browser, "<C-w>" elsewhere
It is empty on a client with nothing to substitute (every terminal, and a browser on macOS, where those shortcuts hang off Cmd and Ctrl arrives untouched).
Every field is false/empty before a client attaches, so check it from UIEnter
rather than at config time: the config is sourced (and VimEnter fires) before
the first client attaches.
Defined in ui.lua.
btv.ui.font(fonts, opts)
btv.ui.font(fonts, opts): set 'guifont' from a font list, tried in order for
every character (wezterm’s font_with_fallback). Each entry is a family name, or a
table with the name first and options of that font’s own:
btv.ui.font({
{ "JetBrains Mono", weight = 500, features = { "+ss01", "-calt" } },
{ "Symbols Nerd Font", scale = 0.9 },
"Noto Color Emoji",
}, { size = 14 })
Per-font options:
weight 1-1000, or "thin" … "normal" … "bold" … "black" (its regular text)
scale a factor on its glyphs' size, in (0, 4]
features OpenType features: "+tag" / "tag" on, "-tag" off, "tag=N" an alternate
opts.size is the point size. Returns the 'guifont' value it set, which the
editor checks like any other (a bad weight or feature tag is E474). The GUI
honors every option; the browser takes the list and the size, and says so when a
font has options it can’t apply there.
Defined in ui.lua.