mirror of
https://github.com/chenasraf/input-form.nvim.git
synced 2026-05-17 17:38:01 +00:00
feat: more style controls in config
This commit is contained in:
73
README.md
73
README.md
@@ -217,16 +217,79 @@ validator = function(value)
|
||||
end
|
||||
```
|
||||
|
||||
### Highlight groups
|
||||
### Select chevrons
|
||||
|
||||
Error rendering uses three highlight groups. Override them to re-theme:
|
||||
The glyphs shown on the right side of `select` inputs to indicate the
|
||||
dropdown state are configurable under `style.chevron`:
|
||||
|
||||
```lua
|
||||
vim.api.nvim_set_hl(0, "InputFormFieldError", { fg = "#ff5555" })
|
||||
vim.api.nvim_set_hl(0, "InputFormFieldErrorBorder", { fg = "#ff5555" })
|
||||
vim.api.nvim_set_hl(0, "InputFormFieldErrorTitle", { fg = "#ff5555", bold = true })
|
||||
require("input-form").setup({
|
||||
style = {
|
||||
chevron = {
|
||||
closed = "⌄", -- default
|
||||
open = "⌃", -- default
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Use whatever you like — e.g. ASCII fallbacks for terminals without good
|
||||
Unicode support:
|
||||
|
||||
```lua
|
||||
style = { chevron = { closed = " v", open = " ^" } }
|
||||
```
|
||||
|
||||
A leading space is recommended so the glyph doesn't sit flush against the
|
||||
label.
|
||||
|
||||
### Highlight groups
|
||||
|
||||
All highlight groups the plugin uses are listed under `style.highlights` in
|
||||
the config and can be overridden via `setup()`. Each entry is passed directly
|
||||
to `vim.api.nvim_set_hl(0, name, spec)`, so anything `nvim_set_hl` accepts
|
||||
works (`fg`, `bg`, `link`, `bold`, `italic`, `default`, etc.).
|
||||
|
||||
```lua
|
||||
require("input-form").setup({
|
||||
style = {
|
||||
highlights = {
|
||||
-- error state
|
||||
InputFormFieldError = { fg = "#ff5555", italic = true },
|
||||
InputFormFieldErrorBorder = { fg = "#ff5555" },
|
||||
InputFormFieldErrorTitle = { fg = "#ff5555", bold = true },
|
||||
-- help footer
|
||||
InputFormHelp = { fg = "#88ccff" },
|
||||
-- parent frame border via link
|
||||
InputFormBorder = { link = "Comment" },
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Available groups:
|
||||
|
||||
| Group | Purpose |
|
||||
| --------------------------- | -------------------------------------------- |
|
||||
| `InputFormNormal` | Parent form window background |
|
||||
| `InputFormBorder` | Parent form border |
|
||||
| `InputFormTitle` | Parent form title |
|
||||
| `InputFormHelp` | Footer help line (key hints) |
|
||||
| `InputFormField` | Individual input field background |
|
||||
| `InputFormFieldBorder` | Individual input field border |
|
||||
| `InputFormFieldTitle` | Individual input field label (on top border) |
|
||||
| `InputFormFieldError` | Error message footer on an invalid field |
|
||||
| `InputFormFieldErrorBorder` | Invalid field border |
|
||||
| `InputFormFieldErrorTitle` | Invalid field label |
|
||||
| `InputFormDropdown` | Select dropdown background |
|
||||
| `InputFormDropdownActive` | Highlighted dropdown row |
|
||||
|
||||
User overrides fully **replace** the default spec per group (they are not
|
||||
deep-merged at the field level), so you don't need to re-specify
|
||||
`default = true`. Highlights are re-applied on every `form:show()`, so a
|
||||
`setup({ style = { highlights = ... } })` call that happens after the first
|
||||
form has been rendered still takes effect on the next open.
|
||||
|
||||
## Configuration
|
||||
|
||||
Defaults:
|
||||
|
||||
@@ -127,6 +127,41 @@ Default configuration for |input-form|.
|
||||
multiline = {
|
||||
height = 5,
|
||||
},
|
||||
--- Visual styling
|
||||
style = {
|
||||
--- Chevron glyphs shown on the right side of `select` inputs to indicate
|
||||
--- the dropdown state. Override either to taste (e.g. `"v"`/`"^"` for
|
||||
--- ASCII, or extra spacing for wider icons).
|
||||
chevron = {
|
||||
--- Glyph shown when the dropdown is closed.
|
||||
closed = "⌄",
|
||||
--- Glyph shown when the dropdown is open.
|
||||
open = "⌃",
|
||||
},
|
||||
--- Highlight groups applied on every `form:show()`. Each entry is passed
|
||||
--- directly to `vim.api.nvim_set_hl(0, name, spec)`, so any option that
|
||||
--- `nvim_set_hl` accepts (`fg`, `bg`, `link`, `bold`, `italic`,
|
||||
--- `default`, ...) is valid. User overrides fully replace the default
|
||||
--- spec for the matching group (they are NOT deep-merged field by field).
|
||||
highlights = {
|
||||
-- Parent form window
|
||||
InputFormNormal = { link = "NormalFloat", default = true },
|
||||
InputFormBorder = { link = "FloatBorder", default = true },
|
||||
InputFormTitle = { link = "FloatTitle", default = true },
|
||||
InputFormHelp = { fg = "Cyan", default = true },
|
||||
-- Individual input fields
|
||||
InputFormField = { link = "NormalFloat", default = true },
|
||||
InputFormFieldBorder = { link = "FloatBorder", default = true },
|
||||
InputFormFieldTitle = { link = "FloatTitle", default = true },
|
||||
-- Error state for individual input fields
|
||||
InputFormFieldError = { fg = "Red", default = true },
|
||||
InputFormFieldErrorBorder = { fg = "Red", default = true },
|
||||
InputFormFieldErrorTitle = { fg = "Red", default = true },
|
||||
-- Select dropdown list
|
||||
InputFormDropdown = { link = "NormalFloat", default = true },
|
||||
InputFormDropdownActive = { link = "PmenuSel", default = true },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
M.options = vim.deepcopy(M.defaults)
|
||||
|
||||
@@ -51,6 +51,41 @@ M.defaults = {
|
||||
multiline = {
|
||||
height = 5,
|
||||
},
|
||||
--- Visual styling
|
||||
style = {
|
||||
--- Chevron glyphs shown on the right side of `select` inputs to indicate
|
||||
--- the dropdown state. Override either to taste (e.g. `"v"`/`"^"` for
|
||||
--- ASCII, or extra spacing for wider icons).
|
||||
chevron = {
|
||||
--- Glyph shown when the dropdown is closed.
|
||||
closed = "⌄",
|
||||
--- Glyph shown when the dropdown is open.
|
||||
open = "⌃",
|
||||
},
|
||||
--- Highlight groups applied on every `form:show()`. Each entry is passed
|
||||
--- directly to `vim.api.nvim_set_hl(0, name, spec)`, so any option that
|
||||
--- `nvim_set_hl` accepts (`fg`, `bg`, `link`, `bold`, `italic`,
|
||||
--- `default`, ...) is valid. User overrides fully replace the default
|
||||
--- spec for the matching group (they are NOT deep-merged field by field).
|
||||
highlights = {
|
||||
-- Parent form window
|
||||
InputFormNormal = { link = "NormalFloat", default = true },
|
||||
InputFormBorder = { link = "FloatBorder", default = true },
|
||||
InputFormTitle = { link = "FloatTitle", default = true },
|
||||
InputFormHelp = { fg = "Cyan", default = true },
|
||||
-- Individual input fields
|
||||
InputFormField = { link = "NormalFloat", default = true },
|
||||
InputFormFieldBorder = { link = "FloatBorder", default = true },
|
||||
InputFormFieldTitle = { link = "FloatTitle", default = true },
|
||||
-- Error state for individual input fields
|
||||
InputFormFieldError = { fg = "Red", default = true },
|
||||
InputFormFieldErrorBorder = { fg = "Red", default = true },
|
||||
InputFormFieldErrorTitle = { fg = "Red", default = true },
|
||||
-- Select dropdown list
|
||||
InputFormDropdown = { link = "NormalFloat", default = true },
|
||||
InputFormDropdownActive = { link = "PmenuSel", default = true },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
M.options = vim.deepcopy(M.defaults)
|
||||
@@ -60,7 +95,16 @@ M.options = vim.deepcopy(M.defaults)
|
||||
---@param user_opts table|nil
|
||||
---@return table
|
||||
function M.setup(user_opts)
|
||||
M.options = utils.merge(vim.deepcopy(M.defaults), user_opts or {})
|
||||
local merged = utils.merge(vim.deepcopy(M.defaults), user_opts or {})
|
||||
-- Highlight specs must be replaced per-group, not deep-merged, so a user
|
||||
-- override like `{ fg = "#ff5555" }` doesn't inherit the default's
|
||||
-- `default = true` flag (which would let a colorscheme clobber it).
|
||||
if user_opts and user_opts.style and user_opts.style.highlights then
|
||||
for name, spec in pairs(user_opts.style.highlights) do
|
||||
merged.style.highlights[name] = spec
|
||||
end
|
||||
end
|
||||
M.options = merged
|
||||
return M.options
|
||||
end
|
||||
|
||||
|
||||
@@ -122,6 +122,10 @@ function M:show()
|
||||
-- form buffers. Runs once per nvim session.
|
||||
utils.register_ui_exclusions()
|
||||
|
||||
-- Apply configured highlight groups (user-configurable via
|
||||
-- `setup({ style = { highlights = { ... } } })`).
|
||||
self:_apply_highlights()
|
||||
|
||||
local layout = self:_compute_layout()
|
||||
self._layout = layout
|
||||
|
||||
@@ -161,9 +165,6 @@ function M:show()
|
||||
win_opts.footer_pos = "center"
|
||||
end
|
||||
end
|
||||
-- Default highlight for the footer (help text): cyan, overridable by the user.
|
||||
pcall(vim.api.nvim_set_hl, 0, "InputFormHelp", { fg = "Cyan", default = true })
|
||||
|
||||
self._parent_win = vim.api.nvim_open_win(self._parent_buf, false, win_opts)
|
||||
vim.wo[self._parent_win].winblend = config.options.window.winblend
|
||||
vim.wo[self._parent_win].winhl = table.concat({
|
||||
@@ -191,16 +192,21 @@ function M:show()
|
||||
self:_install_validation(input)
|
||||
end
|
||||
|
||||
-- Default highlight groups for validation error state. `default = true`
|
||||
-- means user overrides take precedence.
|
||||
pcall(vim.api.nvim_set_hl, 0, "InputFormFieldError", { fg = "Red", default = true })
|
||||
pcall(vim.api.nvim_set_hl, 0, "InputFormFieldErrorBorder", { fg = "Red", default = true })
|
||||
pcall(vim.api.nvim_set_hl, 0, "InputFormFieldErrorTitle", { fg = "Red", default = true })
|
||||
|
||||
self:_focus(1)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Apply all configured highlight groups. Called from `show()` so live
|
||||
--- `setup({ style = { highlights = ... } })` edits take effect on the next
|
||||
--- form open.
|
||||
function M:_apply_highlights()
|
||||
local style = config.options.style or {}
|
||||
local hls = style.highlights or {}
|
||||
for name, spec in pairs(hls) do
|
||||
pcall(vim.api.nvim_set_hl, 0, name, spec)
|
||||
end
|
||||
end
|
||||
|
||||
--- Hide the form (close windows) but keep state so `:show()` can reopen it.
|
||||
function M:hide()
|
||||
if not self._visible then
|
||||
|
||||
@@ -48,12 +48,22 @@ local function label_for(options, id)
|
||||
return ""
|
||||
end
|
||||
|
||||
local CHEVRON_CLOSED = " ▼"
|
||||
local CHEVRON_OPEN = " ▲"
|
||||
-- Fallbacks in case the config module has been mutated to a malformed state.
|
||||
local DEFAULT_CHEVRON_CLOSED = "⌄"
|
||||
local DEFAULT_CHEVRON_OPEN = "⌃"
|
||||
|
||||
local function chevron_for(open)
|
||||
local style = config.options.style or {}
|
||||
local chev = style.chevron or {}
|
||||
if open then
|
||||
return chev.open or DEFAULT_CHEVRON_OPEN
|
||||
end
|
||||
return chev.closed or DEFAULT_CHEVRON_CLOSED
|
||||
end
|
||||
|
||||
local function format_display(options, id, width, open)
|
||||
local label = label_for(options, id)
|
||||
local chevron = open and CHEVRON_OPEN or CHEVRON_CLOSED
|
||||
local chevron = chevron_for(open)
|
||||
if not width or width <= 0 then
|
||||
return label .. chevron
|
||||
end
|
||||
|
||||
@@ -2,6 +2,7 @@ local helpers = dofile("tests/helpers.lua")
|
||||
local MiniTest = require("mini.test")
|
||||
|
||||
local child = helpers.new_child_neovim()
|
||||
local eq = helpers.expect.equality
|
||||
local eq_global, eq_config = helpers.expect.global_equality, helpers.expect.config_equality
|
||||
local eq_type_global, eq_type_config =
|
||||
helpers.expect.global_type_equality, helpers.expect.config_type_equality
|
||||
@@ -35,6 +36,42 @@ T["setup()"]["exposes defaults"] = function()
|
||||
eq_config(child, "multiline.height", 5)
|
||||
end
|
||||
|
||||
T["setup()"]["exposes default highlight groups under style.highlights"] = function()
|
||||
child.lua([[require('input-form').setup()]])
|
||||
eq_type_config(child, "style", "table")
|
||||
eq_type_config(child, "style.highlights", "table")
|
||||
eq_type_config(child, "style.highlights.InputFormHelp", "table")
|
||||
eq_config(child, "style.highlights.InputFormHelp.fg", "Cyan")
|
||||
eq_config(child, "style.highlights.InputFormFieldErrorBorder.fg", "Red")
|
||||
eq_config(child, "style.highlights.InputFormTitle.link", "FloatTitle")
|
||||
end
|
||||
|
||||
T["setup()"]["user highlight overrides replace default specs per group"] = function()
|
||||
helpers.init_plugin(
|
||||
child,
|
||||
[[{
|
||||
style = {
|
||||
highlights = {
|
||||
InputFormHelp = { fg = "#88ccff", italic = true },
|
||||
InputFormFieldErrorBorder = { fg = "#ff5555" },
|
||||
},
|
||||
},
|
||||
}]]
|
||||
)
|
||||
-- Overridden entries take the user's values.
|
||||
eq_config(child, "style.highlights.InputFormHelp.fg", "#88ccff")
|
||||
eq_config(child, "style.highlights.InputFormHelp.italic", true)
|
||||
-- And drop the default's `default = true` flag (replaced per-group, not
|
||||
-- deep-merged).
|
||||
eq(
|
||||
child.lua_get([[require('input-form.config').options.style.highlights.InputFormHelp.default]]),
|
||||
vim.NIL
|
||||
)
|
||||
eq_config(child, "style.highlights.InputFormFieldErrorBorder.fg", "#ff5555")
|
||||
-- Untouched groups keep their defaults.
|
||||
eq_config(child, "style.highlights.InputFormFieldError.fg", "Red")
|
||||
end
|
||||
|
||||
T["setup()"]["deep-merges user options"] = function()
|
||||
helpers.init_plugin(
|
||||
child,
|
||||
|
||||
@@ -34,7 +34,7 @@ T["select input"]["defaults to first option when none given"] = function()
|
||||
eq(child.lua_get([[_G.t:value()]]), "a")
|
||||
local line = child.lua_get([==[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)[1]]==])
|
||||
helpers.expect.match(line, "^Alpha")
|
||||
helpers.expect.match(line, "▼")
|
||||
helpers.expect.match(line, "⌄")
|
||||
end
|
||||
|
||||
T["select input"]["honors explicit default"] = function()
|
||||
@@ -42,7 +42,7 @@ T["select input"]["honors explicit default"] = function()
|
||||
eq(child.lua_get([[_G.t:value()]]), "b")
|
||||
local line = child.lua_get([==[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)[1]]==])
|
||||
helpers.expect.match(line, "^Beta")
|
||||
helpers.expect.match(line, "▼")
|
||||
helpers.expect.match(line, "⌄")
|
||||
end
|
||||
|
||||
T["select input"]["display buffer is read-only"] = function()
|
||||
@@ -60,7 +60,7 @@ T["select input"]["select_id updates value and display"] = function()
|
||||
eq(child.lua_get([[_G.t:value()]]), "c")
|
||||
local line = child.lua_get([==[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)[1]]==])
|
||||
helpers.expect.match(line, "^Gamma")
|
||||
helpers.expect.match(line, "▼")
|
||||
helpers.expect.match(line, "⌄")
|
||||
end
|
||||
|
||||
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()
|
||||
eq(child.lua_get([[_G.t.dropdown_win]]), vim.NIL)
|
||||
end
|
||||
|
||||
T["select input"]["uses custom chevrons from config"] = function()
|
||||
child.lua([[
|
||||
require('input-form').setup({
|
||||
style = { chevron = { closed = " v", open = " ^" } }
|
||||
})
|
||||
_G.t = _G.mk('a')
|
||||
_G.t:mount({ row = 5, col = 5, width = 30 })
|
||||
]])
|
||||
local closed_line = child.lua_get([==[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)[1]]==])
|
||||
helpers.expect.match(closed_line, " v$")
|
||||
helpers.expect.no_match(closed_line, "⌄")
|
||||
-- Flip to open state and re-render.
|
||||
child.lua([[_G.t._open = true; _G.t:_render_display()]])
|
||||
local open_line = child.lua_get([==[vim.api.nvim_buf_get_lines(_G.t.buf, 0, -1, false)[1]]==])
|
||||
helpers.expect.match(open_line, " %^$")
|
||||
helpers.expect.no_match(open_line, "⌃")
|
||||
end
|
||||
|
||||
T["select input"]["rejects empty options list"] = function()
|
||||
local ok = child.lua_get([[
|
||||
(function()
|
||||
|
||||
Reference in New Issue
Block a user