Skip to content

Commit f20343f

Browse files
committed
feat: more style controls in config
1 parent 6a5f8fb commit f20343f

7 files changed

Lines changed: 233 additions & 20 deletions

File tree

README.md

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,16 +217,79 @@ validator = function(value)
217217
end
218218
```
219219

220+
### Select chevrons
221+
222+
The glyphs shown on the right side of `select` inputs to indicate the
223+
dropdown state are configurable under `style.chevron`:
224+
225+
```lua
226+
require("input-form").setup({
227+
style = {
228+
chevron = {
229+
closed = "", -- default
230+
open = "", -- default
231+
},
232+
},
233+
})
234+
```
235+
236+
Use whatever you like — e.g. ASCII fallbacks for terminals without good
237+
Unicode support:
238+
239+
```lua
240+
style = { chevron = { closed = " v", open = " ^" } }
241+
```
242+
243+
A leading space is recommended so the glyph doesn't sit flush against the
244+
label.
245+
220246
### Highlight groups
221247

222-
Error rendering uses three highlight groups. Override them to re-theme:
248+
All highlight groups the plugin uses are listed under `style.highlights` in
249+
the config and can be overridden via `setup()`. Each entry is passed directly
250+
to `vim.api.nvim_set_hl(0, name, spec)`, so anything `nvim_set_hl` accepts
251+
works (`fg`, `bg`, `link`, `bold`, `italic`, `default`, etc.).
223252

224253
```lua
225-
vim.api.nvim_set_hl(0, "InputFormFieldError", { fg = "#ff5555" })
226-
vim.api.nvim_set_hl(0, "InputFormFieldErrorBorder", { fg = "#ff5555" })
227-
vim.api.nvim_set_hl(0, "InputFormFieldErrorTitle", { fg = "#ff5555", bold = true })
254+
require("input-form").setup({
255+
style = {
256+
highlights = {
257+
-- error state
258+
InputFormFieldError = { fg = "#ff5555", italic = true },
259+
InputFormFieldErrorBorder = { fg = "#ff5555" },
260+
InputFormFieldErrorTitle = { fg = "#ff5555", bold = true },
261+
-- help footer
262+
InputFormHelp = { fg = "#88ccff" },
263+
-- parent frame border via link
264+
InputFormBorder = { link = "Comment" },
265+
},
266+
},
267+
})
228268
```
229269

270+
Available groups:
271+
272+
| Group | Purpose |
273+
| --------------------------- | -------------------------------------------- |
274+
| `InputFormNormal` | Parent form window background |
275+
| `InputFormBorder` | Parent form border |
276+
| `InputFormTitle` | Parent form title |
277+
| `InputFormHelp` | Footer help line (key hints) |
278+
| `InputFormField` | Individual input field background |
279+
| `InputFormFieldBorder` | Individual input field border |
280+
| `InputFormFieldTitle` | Individual input field label (on top border) |
281+
| `InputFormFieldError` | Error message footer on an invalid field |
282+
| `InputFormFieldErrorBorder` | Invalid field border |
283+
| `InputFormFieldErrorTitle` | Invalid field label |
284+
| `InputFormDropdown` | Select dropdown background |
285+
| `InputFormDropdownActive` | Highlighted dropdown row |
286+
287+
User overrides fully **replace** the default spec per group (they are not
288+
deep-merged at the field level), so you don't need to re-specify
289+
`default = true`. Highlights are re-applied on every `form:show()`, so a
290+
`setup({ style = { highlights = ... } })` call that happens after the first
291+
form has been rendered still takes effect on the next open.
292+
230293
## Configuration
231294

232295
Defaults:

doc/input-form.txt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,41 @@ Default configuration for |input-form|.
127127
multiline = {
128128
height = 5,
129129
},
130+
--- Visual styling
131+
style = {
132+
--- Chevron glyphs shown on the right side of `select` inputs to indicate
133+
--- the dropdown state. Override either to taste (e.g. `"v"`/`"^"` for
134+
--- ASCII, or extra spacing for wider icons).
135+
chevron = {
136+
--- Glyph shown when the dropdown is closed.
137+
closed = "⌄",
138+
--- Glyph shown when the dropdown is open.
139+
open = "⌃",
140+
},
141+
--- Highlight groups applied on every `form:show()`. Each entry is passed
142+
--- directly to `vim.api.nvim_set_hl(0, name, spec)`, so any option that
143+
--- `nvim_set_hl` accepts (`fg`, `bg`, `link`, `bold`, `italic`,
144+
--- `default`, ...) is valid. User overrides fully replace the default
145+
--- spec for the matching group (they are NOT deep-merged field by field).
146+
highlights = {
147+
-- Parent form window
148+
InputFormNormal = { link = "NormalFloat", default = true },
149+
InputFormBorder = { link = "FloatBorder", default = true },
150+
InputFormTitle = { link = "FloatTitle", default = true },
151+
InputFormHelp = { fg = "Cyan", default = true },
152+
-- Individual input fields
153+
InputFormField = { link = "NormalFloat", default = true },
154+
InputFormFieldBorder = { link = "FloatBorder", default = true },
155+
InputFormFieldTitle = { link = "FloatTitle", default = true },
156+
-- Error state for individual input fields
157+
InputFormFieldError = { fg = "Red", default = true },
158+
InputFormFieldErrorBorder = { fg = "Red", default = true },
159+
InputFormFieldErrorTitle = { fg = "Red", default = true },
160+
-- Select dropdown list
161+
InputFormDropdown = { link = "NormalFloat", default = true },
162+
InputFormDropdownActive = { link = "PmenuSel", default = true },
163+
},
164+
},
130165
}
131166

132167
M.options = vim.deepcopy(M.defaults)

lua/input-form/config.lua

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,41 @@ M.defaults = {
5151
multiline = {
5252
height = 5,
5353
},
54+
--- Visual styling
55+
style = {
56+
--- Chevron glyphs shown on the right side of `select` inputs to indicate
57+
--- the dropdown state. Override either to taste (e.g. `"v"`/`"^"` for
58+
--- ASCII, or extra spacing for wider icons).
59+
chevron = {
60+
--- Glyph shown when the dropdown is closed.
61+
closed = "",
62+
--- Glyph shown when the dropdown is open.
63+
open = "",
64+
},
65+
--- Highlight groups applied on every `form:show()`. Each entry is passed
66+
--- directly to `vim.api.nvim_set_hl(0, name, spec)`, so any option that
67+
--- `nvim_set_hl` accepts (`fg`, `bg`, `link`, `bold`, `italic`,
68+
--- `default`, ...) is valid. User overrides fully replace the default
69+
--- spec for the matching group (they are NOT deep-merged field by field).
70+
highlights = {
71+
-- Parent form window
72+
InputFormNormal = { link = "NormalFloat", default = true },
73+
InputFormBorder = { link = "FloatBorder", default = true },
74+
InputFormTitle = { link = "FloatTitle", default = true },
75+
InputFormHelp = { fg = "Cyan", default = true },
76+
-- Individual input fields
77+
InputFormField = { link = "NormalFloat", default = true },
78+
InputFormFieldBorder = { link = "FloatBorder", default = true },
79+
InputFormFieldTitle = { link = "FloatTitle", default = true },
80+
-- Error state for individual input fields
81+
InputFormFieldError = { fg = "Red", default = true },
82+
InputFormFieldErrorBorder = { fg = "Red", default = true },
83+
InputFormFieldErrorTitle = { fg = "Red", default = true },
84+
-- Select dropdown list
85+
InputFormDropdown = { link = "NormalFloat", default = true },
86+
InputFormDropdownActive = { link = "PmenuSel", default = true },
87+
},
88+
},
5489
}
5590

5691
M.options = vim.deepcopy(M.defaults)
@@ -60,7 +95,16 @@ M.options = vim.deepcopy(M.defaults)
6095
---@param user_opts table|nil
6196
---@return table
6297
function M.setup(user_opts)
63-
M.options = utils.merge(vim.deepcopy(M.defaults), user_opts or {})
98+
local merged = utils.merge(vim.deepcopy(M.defaults), user_opts or {})
99+
-- Highlight specs must be replaced per-group, not deep-merged, so a user
100+
-- override like `{ fg = "#ff5555" }` doesn't inherit the default's
101+
-- `default = true` flag (which would let a colorscheme clobber it).
102+
if user_opts and user_opts.style and user_opts.style.highlights then
103+
for name, spec in pairs(user_opts.style.highlights) do
104+
merged.style.highlights[name] = spec
105+
end
106+
end
107+
M.options = merged
64108
return M.options
65109
end
66110

lua/input-form/form.lua

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ function M:show()
122122
-- form buffers. Runs once per nvim session.
123123
utils.register_ui_exclusions()
124124

125+
-- Apply configured highlight groups (user-configurable via
126+
-- `setup({ style = { highlights = { ... } } })`).
127+
self:_apply_highlights()
128+
125129
local layout = self:_compute_layout()
126130
self._layout = layout
127131

@@ -161,9 +165,6 @@ function M:show()
161165
win_opts.footer_pos = "center"
162166
end
163167
end
164-
-- Default highlight for the footer (help text): cyan, overridable by the user.
165-
pcall(vim.api.nvim_set_hl, 0, "InputFormHelp", { fg = "Cyan", default = true })
166-
167168
self._parent_win = vim.api.nvim_open_win(self._parent_buf, false, win_opts)
168169
vim.wo[self._parent_win].winblend = config.options.window.winblend
169170
vim.wo[self._parent_win].winhl = table.concat({
@@ -191,16 +192,21 @@ function M:show()
191192
self:_install_validation(input)
192193
end
193194

194-
-- Default highlight groups for validation error state. `default = true`
195-
-- means user overrides take precedence.
196-
pcall(vim.api.nvim_set_hl, 0, "InputFormFieldError", { fg = "Red", default = true })
197-
pcall(vim.api.nvim_set_hl, 0, "InputFormFieldErrorBorder", { fg = "Red", default = true })
198-
pcall(vim.api.nvim_set_hl, 0, "InputFormFieldErrorTitle", { fg = "Red", default = true })
199-
200195
self:_focus(1)
201196
return self
202197
end
203198

199+
--- Apply all configured highlight groups. Called from `show()` so live
200+
--- `setup({ style = { highlights = ... } })` edits take effect on the next
201+
--- form open.
202+
function M:_apply_highlights()
203+
local style = config.options.style or {}
204+
local hls = style.highlights or {}
205+
for name, spec in pairs(hls) do
206+
pcall(vim.api.nvim_set_hl, 0, name, spec)
207+
end
208+
end
209+
204210
--- Hide the form (close windows) but keep state so `:show()` can reopen it.
205211
function M:hide()
206212
if not self._visible then

lua/input-form/inputs/select.lua

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,22 @@ local function label_for(options, id)
4848
return ""
4949
end
5050

51-
local CHEVRON_CLOSED = ""
52-
local CHEVRON_OPEN = ""
51+
-- Fallbacks in case the config module has been mutated to a malformed state.
52+
local DEFAULT_CHEVRON_CLOSED = ""
53+
local DEFAULT_CHEVRON_OPEN = ""
54+
55+
local function chevron_for(open)
56+
local style = config.options.style or {}
57+
local chev = style.chevron or {}
58+
if open then
59+
return chev.open or DEFAULT_CHEVRON_OPEN
60+
end
61+
return chev.closed or DEFAULT_CHEVRON_CLOSED
62+
end
5363

5464
local function format_display(options, id, width, open)
5565
local label = label_for(options, id)
56-
local chevron = open and CHEVRON_OPEN or CHEVRON_CLOSED
66+
local chevron = chevron_for(open)
5767
if not width or width <= 0 then
5868
return label .. chevron
5969
end

tests/test_config.lua

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ local helpers = dofile("tests/helpers.lua")
22
local MiniTest = require("mini.test")
33

44
local child = helpers.new_child_neovim()
5+
local eq = helpers.expect.equality
56
local eq_global, eq_config = helpers.expect.global_equality, helpers.expect.config_equality
67
local eq_type_global, eq_type_config =
78
helpers.expect.global_type_equality, helpers.expect.config_type_equality
@@ -35,6 +36,42 @@ T["setup()"]["exposes defaults"] = function()
3536
eq_config(child, "multiline.height", 5)
3637
end
3738

39+
T["setup()"]["exposes default highlight groups under style.highlights"] = function()
40+
child.lua([[require('input-form').setup()]])
41+
eq_type_config(child, "style", "table")
42+
eq_type_config(child, "style.highlights", "table")
43+
eq_type_config(child, "style.highlights.InputFormHelp", "table")
44+
eq_config(child, "style.highlights.InputFormHelp.fg", "Cyan")
45+
eq_config(child, "style.highlights.InputFormFieldErrorBorder.fg", "Red")
46+
eq_config(child, "style.highlights.InputFormTitle.link", "FloatTitle")
47+
end
48+
49+
T["setup()"]["user highlight overrides replace default specs per group"] = function()
50+
helpers.init_plugin(
51+
child,
52+
[[{
53+
style = {
54+
highlights = {
55+
InputFormHelp = { fg = "#88ccff", italic = true },
56+
InputFormFieldErrorBorder = { fg = "#ff5555" },
57+
},
58+
},
59+
}]]
60+
)
61+
-- Overridden entries take the user's values.
62+
eq_config(child, "style.highlights.InputFormHelp.fg", "#88ccff")
63+
eq_config(child, "style.highlights.InputFormHelp.italic", true)
64+
-- And drop the default's `default = true` flag (replaced per-group, not
65+
-- deep-merged).
66+
eq(
67+
child.lua_get([[require('input-form.config').options.style.highlights.InputFormHelp.default]]),
68+
vim.NIL
69+
)
70+
eq_config(child, "style.highlights.InputFormFieldErrorBorder.fg", "#ff5555")
71+
-- Untouched groups keep their defaults.
72+
eq_config(child, "style.highlights.InputFormFieldError.fg", "Red")
73+
end
74+
3875
T["setup()"]["deep-merges user options"] = function()
3976
helpers.init_plugin(
4077
child,

tests/test_inputs_select.lua

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ T["select input"]["defaults to first option when none given"] = function()
3434
eq(child.lua_get([[_G.t:value()]]), "a")
3535
local line = child.lua_get([==[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)[1]]==])
3636
helpers.expect.match(line, "^Alpha")
37-
helpers.expect.match(line, "")
37+
helpers.expect.match(line, "")
3838
end
3939

4040
T["select input"]["honors explicit default"] = function()
4141
child.lua([[_G.t = _G.mk('b'); _G.t:mount({ row = 5, col = 5, width = 30 })]])
4242
eq(child.lua_get([[_G.t:value()]]), "b")
4343
local line = child.lua_get([==[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)[1]]==])
4444
helpers.expect.match(line, "^Beta")
45-
helpers.expect.match(line, "")
45+
helpers.expect.match(line, "")
4646
end
4747

4848
T["select input"]["display buffer is read-only"] = function()
@@ -60,7 +60,7 @@ T["select input"]["select_id updates value and display"] = function()
6060
eq(child.lua_get([[_G.t:value()]]), "c")
6161
local line = child.lua_get([==[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)[1]]==])
6262
helpers.expect.match(line, "^Gamma")
63-
helpers.expect.match(line, "")
63+
helpers.expect.match(line, "")
6464
end
6565

6666
T["select input"]["open_dropdown shows all options and <CR> confirms"] = function()
@@ -95,6 +95,24 @@ T["select input"]["<Esc> closes dropdown without changing value"] = function()
9595
eq(child.lua_get([[_G.t.dropdown_win]]), vim.NIL)
9696
end
9797

98+
T["select input"]["uses custom chevrons from config"] = function()
99+
child.lua([[
100+
require('input-form').setup({
101+
style = { chevron = { closed = " v", open = " ^" } }
102+
})
103+
_G.t = _G.mk('a')
104+
_G.t:mount({ row = 5, col = 5, width = 30 })
105+
]])
106+
local closed_line = child.lua_get([==[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)[1]]==])
107+
helpers.expect.match(closed_line, " v$")
108+
helpers.expect.no_match(closed_line, "")
109+
-- Flip to open state and re-render.
110+
child.lua([[_G.t._open = true; _G.t:_render_display()]])
111+
local open_line = child.lua_get([==[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)[1]]==])
112+
helpers.expect.match(open_line, " %^$")
113+
helpers.expect.no_match(open_line, "")
114+
end
115+
98116
T["select input"]["rejects empty options list"] = function()
99117
local ok = child.lua_get([[
100118
(function()

0 commit comments

Comments
 (0)