Skip to content

Commit 1363f6b

Browse files
committed
feat: keymaps window instead of help line
1 parent 0424bdf commit 1363f6b

5 files changed

Lines changed: 369 additions & 108 deletions

File tree

README.md

Lines changed: 117 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
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({
8386
form: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
9193
vim.keymap.set("n", "<leader>xf", function()
@@ -95,19 +97,19 @@ end)
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:
173194
validator = 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
241261
validator = function(value)
@@ -245,7 +265,7 @@ validator = function(value)
245265
end
246266
```
247267

248-
### Checkbox glyphs
268+
### Checkbox glyphs and padding
249269

250270
Override 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
272304
require("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
286317
style = { 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
300329
require("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

371410
Per-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

401440
Key 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
{

doc/input-form.txt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,16 @@ Default configuration for |input-form|.
113113
prev = "<S-Tab>",
114114
--- Submit the form and invoke `on_submit(results)`.
115115
submit = "<C-s>",
116-
--- Cancel the form and invoke `on_cancel()` if provided.
117-
cancel = "<Esc>",
116+
--- Cancel the form and invoke `on_cancel()` if provided. Accepts a
117+
--- single key string or a list of keys — all listed keys trigger cancel.
118+
cancel = { "<Esc>", "q" },
118119
--- Open the dropdown when focused on a `select` input.
119120
open_select = "<CR>",
120121
--- Toggle the value of a `checkbox` input.
121122
toggle = "<Space>",
123+
--- Toggle a help popup listing every active keymap. The popup opens
124+
--- directly below the form window and closes on the same key.
125+
help = "?",
122126
},
123127
--- Options for `select` inputs.
124128
select = {

lua/input-form/config.lua

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,16 @@ M.defaults = {
3737
prev = "<S-Tab>",
3838
--- Submit the form and invoke `on_submit(results)`.
3939
submit = "<C-s>",
40-
--- Cancel the form and invoke `on_cancel()` if provided.
41-
cancel = "<Esc>",
40+
--- Cancel the form and invoke `on_cancel()` if provided. Accepts a
41+
--- single key string or a list of keys — all listed keys trigger cancel.
42+
cancel = { "<Esc>", "q" },
4243
--- Open the dropdown when focused on a `select` input.
4344
open_select = "<CR>",
4445
--- Toggle the value of a `checkbox` input.
4546
toggle = "<Space>",
47+
--- Toggle a help popup listing every active keymap. The popup opens
48+
--- directly below the form window and closes on the same key.
49+
help = "?",
4650
},
4751
--- Options for `select` inputs.
4852
select = {

0 commit comments

Comments
 (0)