11# input-form.nvim
22
3- A small Neovim plugin for building bordered, keyboard-navigable ** forms** in a
4- floating window. Create a single window containing multiple typed inputs
5- (single-line text, multiline text, select dropdowns), collect results via an
6- ` on_submit ` callback.
3+ A small Neovim plugin for building bordered, keyboard-navigable ** forms** in a floating window.
4+ Create a single window containing multiple typed inputs (single-line text, multiline text, select
5+ dropdowns, checkboxes), collect results via an ` on_submit ` callback.
76
87![ input form example] ( /input-form.gif )
98
109## Features
1110
1211- Bordered floating window with optional title
1312- Keyboard-navigable: ` <Tab> ` / ` <S-Tab> ` to move between inputs
14- - Input types: ` text ` , ` multiline ` , ` select ` , ` checkbox `
13+ - Input types: ` text ` , ` multiline ` , ` select ` , ` checkbox ` , plus ` spacer ` (visual-only gap between
14+ fields)
1515- Select dropdowns open with ` <CR> ` ; arrows navigate; ` <CR> ` confirms
16+ - Checkbox toggles with ` <Space> ` or ` <CR> `
1617- Submit with ` <C-s> ` — results delivered as a ` { [name] = value } ` table
17- - Cancel with ` <Esc> `
18+ - Cancel with ` <Esc> ` or ` q `
19+ - Built-in toggleable help popup (` ? ` ) listing every active keymap — updates automatically when you
20+ remap keys
1821- Lazy: ` create_form ` builds the form; ` :show() ` renders it when you want
1922- ` :hide() ` / ` :show() ` re-open a form while preserving in-progress values
20- - Fully configurable keymaps, border, width, title
23+ - Fully configurable keymaps (strings or lists) , border, width, title
2124- Auto-generated help doc (` :h input-form ` )
2225- Tested with ` mini.test `
2326
@@ -83,9 +86,8 @@ local form = f.create_form({
8386form :show ()
8487```
8588
86- ` create_form ` returns a form object. Nothing is rendered until you call
87- ` form:show() ` . This lets you construct the form in one place and open it from a
88- keymap, autocommand, or anywhere else:
89+ ` create_form ` returns a form object. Nothing is rendered until you call ` form:show() ` . This lets you
90+ construct the form in one place and open it from a keymap, autocommand, or anywhere else:
8991
9092``` lua
9193vim .keymap .set (" n" , " <leader>xf" , function ()
9597
9698### Form methods
9799
98- | Method | Description |
99- | -------------- | ---------------------------------------------------------------- |
100- | ` form:show() ` | Open the form. No-op if already visible. |
101- | ` form:hide() ` | Close windows but keep values so ` :show() ` resumes where you left off. |
102- | ` form:close() ` | Permanently tear down the form. |
103- | ` form:submit() ` | Gather values, close, and invoke ` on_submit(results) ` . |
104- | ` form:cancel() ` | Close and invoke ` on_cancel() ` if provided. |
105- | ` form:results() ` | Return ` { [name] = value } ` without closing. |
100+ | Method | Description |
101+ | ---------------- | ------ ---------------------------------------------------------------- |
102+ | ` form:show() ` | Open the form. No-op if already visible. |
103+ | ` form:hide() ` | Close windows but keep values so ` :show() ` resumes where you left off. |
104+ | ` form:close() ` | Permanently tear down the form. |
105+ | ` form:submit() ` | Gather values, close, and invoke ` on_submit(results) ` . |
106+ | ` form:cancel() ` | Close and invoke ` on_cancel() ` if provided. |
107+ | ` form:results() ` | Return ` { [name] = value } ` without closing. |
106108
107109### Input spec reference
108110
109- All inputs share ` name ` (string, required — the key in the result table) and
110- ` label ` (string, shown above the field) .
111+ Most inputs share ` name ` (string, required — the key in the result table) and ` label ` (string, shown
112+ above the field). ` spacer ` is the only exception: it's visual-only and needs no ` name ` .
111113
112114#### ` text `
113115
@@ -121,8 +123,7 @@ All inputs share `name` (string, required — the key in the result table) and
121123{ name = " body" , label = " Notes" , type = " multiline" , default = " " , height = 5 }
122124```
123125
124- - ` height ` (optional) — number of rows for the input; falls back to
125- ` config.multiline.height ` .
126+ - ` height ` (optional) — number of rows for the input; falls back to ` config.multiline.height ` .
126127
127128#### ` select `
128129
@@ -147,23 +148,43 @@ All inputs share `name` (string, required — the key in the result table) and
147148{ name = " agree" , label = " I agree" , type = " checkbox" , default = false }
148149```
149150
150- Unlike text/multiline/select, checkboxes render ** inline** — no border, no
151- separate label row. The glyph sits immediately next to the label, and any
152- validation error is appended on the same line:
151+ Unlike text/multiline/select, checkboxes render ** inline** — no border, no separate label row. The
152+ glyph sits immediately next to the label, and any validation error is appended on the same line:
153153
154154```
155155☐ I agree (must be checked)
156156```
157157
158158- ` default ` (optional) — boolean (defaults to ` false ` ).
159159- ` value() ` returns a boolean.
160- - Toggled with the configured ` keymaps.toggle ` key (default ` <Space> ` ) or
161- the ` keymaps.open_select ` key (default ` <CR> ` ) — both work so users get
162- a consistent "interact with this field" key.
163- - Glyphs come from ` style.checkbox.{checked, unchecked} ` (defaults
164- ` "☑" ` / ` "☐" ` ).
165- - Pair with ` validators.checked() ` to require the box to be ticked (see
166- [ Validation] ( #validation ) ).
160+ - Toggled with the configured ` keymaps.toggle ` key (default ` <Space> ` ) or the ` keymaps.open_select `
161+ key (default ` <CR> ` ) — both work so users get a consistent "interact with this field" key.
162+ - Glyphs come from ` style.checkbox.{checked, unchecked} ` (defaults ` "☑" ` / ` "☐" ` ).
163+ - A blank row is rendered above and below each checkbox to visually separate it from adjacent
164+ bordered inputs. Tune via ` style.checkbox.padding ` (default ` 1 ` , set to ` 0 ` to pack tight).
165+ - Pair with ` validators.checked() ` to require the box to be ticked (see [ Validation] ( #validation ) ).
166+
167+ #### ` spacer `
168+
169+ ``` lua
170+ { type = " spacer" } -- 1 blank row
171+ { type = " spacer" , height = 2 } -- 2 blank rows
172+ ```
173+
174+ A visual-only faux input that reserves blank rows in the layout. It has no window, no focus, no
175+ validator, and never appears in ` results() ` . Use it to group related inputs visually:
176+
177+ ``` lua
178+ inputs = {
179+ { name = " first" , label = " First name" , type = " text" },
180+ { name = " last" , label = " Last name" , type = " text" },
181+ { type = " spacer" },
182+ { name = " email" , label = " Email" , type = " text" },
183+ }
184+ ```
185+
186+ - ` height ` (optional) — number of blank rows (default ` 1 ` ).
187+ - ` <Tab> ` / ` <S-Tab> ` skip over spacers automatically.
167188
168189## Validation
169190
@@ -173,16 +194,15 @@ Each input spec accepts an optional `validator` function:
173194validator = fun (value : any ): string |nil
174195```
175196
176- Return a non-empty error message string to mark the input invalid, or ` nil ` /
177- ` "" ` when valid. The error message is shown in the input's bottom border
178- (red), and the border + label turn red too. Validation runs:
197+ Return a non-empty error message string to mark the input invalid, or ` nil ` / ` "" ` when valid. The
198+ error message is shown in the input's bottom border (red), and the border + label turn red too.
199+ Validation runs:
179200
180- - ** On blur** — the first time the user leaves the field it is marked
181- "touched" and the validator runs. Nothing is shown before that.
201+ - ** On blur** — the first time the user leaves the field it is marked "touched" and the validator
202+ runs. Nothing is shown before that.
182203- ** On change** — once touched, each buffer change re-runs the validator.
183- - ** On submit** — ` form:submit() ` force-validates every input (touched or
184- not). If any input has an error, submission is blocked, all errors are
185- rendered, and focus moves to the first invalid input.
204+ - ** On submit** — ` form:submit() ` force-validates every input (touched or not). If any input has an
205+ error, submission is blocked, all errors are rendered, and focus moves to the first invalid input.
186206
187207### Built-in validators
188208
@@ -234,8 +254,8 @@ f.create_form({
234254}):show ()
235255```
236256
237- Custom validators are just functions — no need to use the builder helpers if
238- you'd rather write one inline:
257+ Custom validators are just functions — no need to use the builder helpers if you'd rather write one
258+ inline:
239259
240260``` lua
241261validator = function (value )
@@ -245,7 +265,7 @@ validator = function(value)
245265end
246266```
247267
248- ### Checkbox glyphs
268+ ### Checkbox glyphs and padding
249269
250270Override the characters rendered by ` checkbox ` inputs via ` style.checkbox ` :
251271
@@ -255,18 +275,30 @@ require("input-form").setup({
255275 checkbox = {
256276 checked = " ☑" , -- default
257277 unchecked = " ☐" , -- default
278+ padding = 1 , -- default: blank rows above/below each checkbox
258279 },
259280 },
260281})
261282```
262283
263- Alternatives that render well in most fonts: ` [x] ` / ` [ ] ` , ` ✔ ` / ` · ` ,
264- ` ● ` / ` ○ ` .
284+ Alternatives that render well in most fonts: ` [x] ` / ` [ ] ` , ` ✔ ` / ` · ` , ` ● ` / ` ○ ` . Set ` padding = 0 `
285+ to pack checkboxes flush against adjacent bordered inputs.
286+
287+ ### Help popup
288+
289+ The form's bottom border shows a compact ` ? help ` hint on the right. Press ` ? ` (configurable via
290+ ` keymaps.help ` ) to toggle a floating popup below the form that lists ** every active keymap** — it
291+ reads from ` config.keymaps ` at render time, so if you remap ` submit ` to ` <C-Enter> ` the popup
292+ reflects that automatically. The popup matches the form's width, wraps long descriptions, and flips
293+ above the form if it would overflow the editor bottom. It only lists keys relevant to the current
294+ form (e.g. ` toggle checkbox ` only appears when a checkbox is present).
295+
296+ Set ` keymaps.help = false ` to disable both the popup and the footer hint.
265297
266298### Select chevrons
267299
268- The glyphs shown on the right side of ` select ` inputs to indicate the
269- dropdown state are configurable under ` style.chevron ` :
300+ The glyphs shown on the right side of ` select ` inputs to indicate the dropdown state are
301+ configurable under ` style.chevron ` :
270302
271303``` lua
272304require (" input-form" ).setup ({
@@ -279,22 +311,19 @@ require("input-form").setup({
279311})
280312```
281313
282- Use whatever you like — e.g. ASCII fallbacks for terminals without good
283- Unicode support:
314+ Use whatever you like — e.g. ASCII fallbacks for terminals without good Unicode support:
284315
285316``` lua
286317style = { chevron = { closed = " v" , open = " ^" } }
287318```
288319
289- A leading space is recommended so the glyph doesn't sit flush against the
290- label.
320+ A leading space is recommended so the glyph doesn't sit flush against the label.
291321
292322### Highlight groups
293323
294- All highlight groups the plugin uses are listed under ` style.highlights ` in
295- the config and can be overridden via ` setup() ` . Each entry is passed directly
296- to ` vim.api.nvim_set_hl(0, name, spec) ` , so anything ` nvim_set_hl ` accepts
297- works (` fg ` , ` bg ` , ` link ` , ` bold ` , ` italic ` , ` default ` , etc.).
324+ All highlight groups the plugin uses are listed under ` style.highlights ` in the config and can be
325+ overridden via ` setup() ` . Each entry is passed directly to ` vim.api.nvim_set_hl(0, name, spec) ` , so
326+ anything ` nvim_set_hl ` accepts works (` fg ` , ` bg ` , ` link ` , ` bold ` , ` italic ` , ` default ` , etc.).
298327
299328``` lua
300329require (" input-form" ).setup ({
@@ -320,7 +349,7 @@ Available groups:
320349| ` InputFormNormal ` | Parent form window background |
321350| ` InputFormBorder ` | Parent form border |
322351| ` InputFormTitle ` | Parent form title |
323- | ` InputFormHelp ` | Footer help line (key hints) |
352+ | ` InputFormHelp ` | Footer ` ? help` hint on the form border |
324353| ` InputFormField ` | Individual input field background |
325354| ` InputFormFieldBorder ` | Individual input field border |
326355| ` InputFormFieldTitle ` | Individual input field label (on top border) |
@@ -330,11 +359,10 @@ Available groups:
330359| ` InputFormDropdown ` | Select dropdown background |
331360| ` InputFormDropdownActive ` | Highlighted dropdown row |
332361
333- User overrides fully ** replace** the default spec per group (they are not
334- deep-merged at the field level), so you don't need to re-specify
335- ` default = true ` . Highlights are re-applied on every ` form:show() ` , so a
336- ` setup({ style = { highlights = ... } }) ` call that happens after the first
337- form has been rendered still takes effect on the next open.
362+ User overrides fully ** replace** the default spec per group (they are not deep-merged at the field
363+ level), so you don't need to re-specify ` default = true ` . Highlights are re-applied on every
364+ ` form:show() ` , so a ` setup({ style = { highlights = ... } }) ` call that happens after the first form
365+ has been rendered still takes effect on the next open.
338366
339367## Configuration
340368
@@ -352,37 +380,48 @@ require("input-form").setup({
352380 gap = 0 , -- blank rows between adjacent inputs
353381 },
354382 keymaps = {
383+ -- Every keymap accepts either a single key string or a list of keys.
384+ -- Set any value to `false` to disable.
355385 next = " <Tab>" ,
356386 prev = " <S-Tab>" ,
357387 submit = " <C-s>" ,
358- cancel = " <Esc>" ,
388+ cancel = { " <Esc>" , " q " }, -- list form: both keys cancel the form
359389 open_select = " <CR>" ,
360390 toggle = " <Space>" ,
391+ help = " ?" , -- toggle the help popup (set `false` to hide)
361392 },
362393 select = {
363394 max_height = 10 ,
364395 },
365396 multiline = {
366397 height = 5 ,
367398 },
399+ style = {
400+ checkbox = {
401+ checked = " ☑" ,
402+ unchecked = " ☐" ,
403+ padding = 1 , -- blank rows above/below each checkbox
404+ },
405+ -- ...chevron, highlights, etc. — see sections above.
406+ },
368407})
369408```
370409
371410Per-form overrides: pass ` title ` and/or ` width ` in the ` create_form ` spec.
372411
373412## Help
374413
375- Help tags are registered automatically on the first ` require('input-form') ` ,
376- so ` setup() ` is not required for them either:
414+ Help tags are registered automatically on the first ` require('input-form') ` , so ` setup() ` is not
415+ required for them either:
377416
378417```
379418:h input-form
380419```
381420
382421## For plugin developers — using input-form.nvim as a dependency
383422
384- You can depend on ` input-form.nvim ` from another plugin without forcing your
385- users to call ` setup() ` . The module is safe to use immediately after require:
423+ You can depend on ` input-form.nvim ` from another plugin without forcing your users to call
424+ ` setup() ` . The module is safe to use immediately after require:
386425
387426``` lua
388427-- In your plugin's code:
@@ -400,19 +439,18 @@ input_form.create_form({
400439
401440Key points:
402441
403- - ** No ` setup() ` required.** Defaults are loaded at module-load time and
404- ` create_form ` / ` form:show() ` work on a bare ` require('input-form') ` . End
405- users of your plugin don't need to know input-form.nvim exists.
406- - ** Per-form overrides.** Pass ` title ` , ` width ` , ` on_cancel ` , etc. directly in
407- the ` create_form ` spec — no need to mutate global config for one-off tweaks.
408- - ** Baseline config.** If your plugin wants a different baseline (say, a
409- non-default border style for all forms it opens), call
410- ` require('input-form').setup({ ... }) ` once during your plugin's own
411- initialization. This is idempotent and safe to call even if the end user
412- has already called setup — later calls deep-merge over earlier ones.
413- - ** Respect the user.** Prefer per-form overrides over global ` setup() ` when
414- possible so you don't stomp on a user who has configured input-form.nvim
415- for their own keymaps or other plugins that use it.
442+ - ** No ` setup() ` required.** Defaults are loaded at module-load time and ` create_form ` /
443+ ` form:show() ` work on a bare ` require('input-form') ` . End users of your plugin don't need to know
444+ input-form.nvim exists.
445+ - ** Per-form overrides.** Pass ` title ` , ` width ` , ` on_cancel ` , etc. directly in the ` create_form `
446+ spec — no need to mutate global config for one-off tweaks.
447+ - ** Baseline config.** If your plugin wants a different baseline (say, a non-default border style
448+ for all forms it opens), call ` require('input-form').setup({ ... }) ` once during your plugin's own
449+ initialization. This is idempotent and safe to call even if the end user has already called setup
450+ — later calls deep-merge over earlier ones.
451+ - ** Respect the user.** Prefer per-form overrides over global ` setup() ` when possible so you don't
452+ stomp on a user who has configured input-form.nvim for their own keymaps or other plugins that use
453+ it.
416454- ** Declaring the dep.** With lazy.nvim, add it to your ` dependencies ` :
417455 ``` lua
418456 {
0 commit comments