Skip to content

Instantly share code, notes, and snippets.

@ef3d0c3e
Last active July 9, 2026 01:23
Show Gist options
  • Select an option

  • Save ef3d0c3e/3ca866a7383e13850d1d9f4a24643694 to your computer and use it in GitHub Desktop.

Select an option

Save ef3d0c3e/3ca866a7383e13850d1d9f4a24643694 to your computer and use it in GitHub Desktop.
Create a fancy NeoVim menu

Have you ever had trouble remembering keybinds, commands or functions for features you rarely use?

Have you ever wished you could make better use of the hundred of features that come with plugins like snacks.nvim? Wished you remembered keybinds for all pickers, menus and options...

In this gist, I'll show you how to create a simple and useful menu for your neovim configurations, using Snacks.nvim and (optional) which-key.nvim.

In 200 lines you will learn how to create menu that is searchable, displays keybinds and lets you easily find what you're looking for, without sifting through :h <option>.

Menu demo

Features:

  • Fuzzy finder for options thanks to snacks picker
  • Icons and tags to make categories easier to distinguish
  • Automatic keybinds, when you require them
  • Fully integrated with which-key.nvim, your options descriptions and icons will be visible in which-key
  • Extensible at will

Setup

Put the menu.lua somwhere in your .config/nvim/lua

To use the menu, you must first initialize the module (e.g in your init.lua):

require("menu").setup()

Then bind menu.show() to a key:

wk.add({ "<leader>e", function() require("menu").show() end, desc = "Menu" })

Of course, you'll need to change require("menu") to the module path of the script.

I greatly encourage you to read, learn and modify the script as it's very short and easy to understand. It's simple to extend and add new features, as you require them.

Adding options

Adding options is as simple as adding a new entry in the options table. Menu options can be customized at will to perform any kind of actions when selected.

The script exposes these two functions to make your life easier:

MenuAction

This is for options that simply run a command or function when selected in the menu.

MenuAction {
    -- Optional keybind for this action, accept an array of strings if you want to set multiple keybinds
    key = "<leader>uC",
    -- Description of the option
    desc = "Colorschemes",
    -- Optional icon, will use a default icon if unset
    icon = "",
    -- Optional list of tags, to make searching and categorizing easier
    tags = { "ui", "menu" },
    -- Function called when this option is selected
    callback = functiopn() Snacks.picker.colorschemes() end
},

MenuToggle

This is for toggle-able actions.

MenuToggle {
    -- Optional keybind for this action, accept an array of strings if you want to set multiple keybinds
    key = "<leader>un",
    -- Description of the option
    desc = "Toggle line numbers",
    -- Optional list of tags, to make searching and categorizing easier
    tags = { "ui" },
    -- Function to check if this option is enabled or disabled, must return a boolean
    is_active = function()
        return vim.api.nvim_get_option_value("number", { scope = "local" })
    end,
    -- Function called when this option is selected, capture argument `self`
    callback = function(self)
        if self:is_active() then
            vim.opt.number = false
            vim.opt.relativenumber = false
        else
            vim.opt.number = true
            vim.opt.relativenumber = true
        end
    end,
},

In all options, you can add a init field that must contain a function that will be called when setup() is called on the module. All functions are called with self as their first argument so you may use it for more complex options.

That's all, hope you enjoy building menus with this simple script.

local M = {}
-- {{{ Library
-- Display tags
local function show_tags(tags)
if not tags or #tags == 0 then return "" end
return " +" .. table.concat(tags, " +");
end
-- Display keybinds
local function show_keys(keys)
if not keys then return "" end
local function format(key)
-- Alternatively, replace 'spc-' with your <leader> key name
return string.gsub(key, "<leader>", "spc-")
end
if type(keys) == 'table' then
local result = ""
for _, key in ipairs(keys) do
if #result > 0 then
result = result .. " " .. format(key)
else
result = format(key)
end
end
return " " .. result
else
return " " .. format(keys)
end
end
-- Format commands to start with ':'
local function show_cmd(cmd)
if not cmd then return "" end
return " :" .. cmd
end
local function get_icon(table, default)
if table.icon then
if type(table.icon) == "table" then
assert(#table.icon == 2)
return table.icon
end
return { table.icon, default[2] }
end
return default
end
-- Run function on select
local function MenuAction(table)
table._init = function(self)
if self.init then self:init() end
local tags = show_tags(self.tags)
local keys = show_keys(self.key)
local cmd = show_cmd(self.command)
self.text = self.desc .. (tags or "") .. (keys or "") .. (cmd or "")
self._wk_icon = get_icon(self, { "󱐌 ", "" })[1]
end
table.display = function(self)
local tags = show_tags(self.tags)
local keys = show_keys(self.key)
local cmd = show_cmd(self.command)
local icon = get_icon(self, { "󱐌 ", "@diff.delta" })
return {
icon,
{ " " },
{ self.desc },
{ tags, "Comment", },
{ keys, "String", },
{ cmd, "String", },
}
end
if table.command and not table.callback then
table.callback = function(self)
vim.cmd(self.command)
end
end
return table
end
-- Toggle option on select
local function MenuToggle(table)
table._init = function(self)
if self.init then self:init() end
local tags = show_tags(self.tags)
local keys = show_keys(self.key)
local cmd = show_cmd(self.command)
self.text = self.desc .. (tags or "") .. (keys or "") .. (cmd or "")
table._wk_icon = get_icon(self, { "󰦐 ", "" })[1]
self._is_active = self:is_active()
end
local _callback = table.callback
table.callback = function(self)
_callback(self)
self._is_active = self:is_active()
end
table.display = function(self)
local tags = show_tags(self.tags)
local keys = show_keys(self.key)
local cmd = show_cmd(self.command)
local icon = self._is_active and "" or ""
local icon_hi = self._is_active and "@diff.plus" or "@diff.minus"
return {
{ icon, icon_hi },
{ " " },
{ self.desc },
{ tags, "Comment", },
{ keys, "String", },
{ cmd, "String", },
}
end
return table
end
-- }}}
-- All menu options
local options = {
-- Evaluate a function on select:
MenuAction {
key = "<leader>t", -- Keybind
desc = "Open Terminal", -- Description
icon = "", -- (Optional) icon
tags = { "ui", "terminal" }, -- (Optional) tags
callback = function() -- Function called on select
vim.cmd.term()
vim.cmd.startinsert()
end
},
-- Another example:
MenuAction {
key = { "<leader>uf", "<leader>lF" }, -- You can set multiple keybinds
desc = "Format buffer",
tags = { "lsp" },
callback = function() vim.lsp.buf.format({ async = false }) end
},
-- A toggle:
MenuToggle {
key = "<leader>un", -- Keybind
desc = "Toggle line numbers", -- Description
tags = { "ui" }, -- Tags
icon = "", -- Optional icon
is_active = function() -- Function to check if option is active, required for `MenuToggle`
return vim.api.nvim_get_option_value("number", { scope = "local" })
end,
callback = function(self) -- Callback on select
if self:is_active() then
vim.opt.number = false
vim.opt.relativenumber = false
else
vim.opt.number = true
vim.opt.relativenumber = true
end
end,
},
-- Evaluate commands directly:
MenuAction { icon = "", desc = "Add snippet", tags = { "snippets" }, command = "ScissorsAddNewSnippet", },
MenuAction { icon = "", desc = "Edit snippet", tags = { "snippets" }, command = "ScissorsEditSnippet", },
}
-- Entry point: show snacks menu
function M.show()
---@diagnostic disable-next-line: undefined-global
Snacks.picker({
items = options, -- The list of menu items
title = "Menu", -- Menu title
layout = "select",
format = function(item)
return item:display()
end,
actions = {
confirm = function(picker)
local item = picker:selected({ fallback = true })[1]
picker:close()
if item then
item:callback()
end
end,
},
})
end
-- Call `_init` on components and setup keybinds
function M.setup()
-- Which-key integration, can be replaced with neovim's builtin keybinds if wanted
local wk = require("which-key")
local table = {}
for _, entry in ipairs(options) do
entry:_init()
if entry.key then
if type(entry.key) == 'table' then
for _, v in ipairs(entry.key) do
table[#table + 1] = { v, function() entry:callback() end, desc = entry.desc, icon = entry._wk_icon }
end
else
table[#table + 1] = { entry.key, function() entry:callback() end, desc = entry.desc, icon = entry
._wk_icon }
end
end
end
wk.add(table)
end
return M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment