Skip to content

mjmjm0101/quickui.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

quickui.nvim

It provides a UI — but the goal isn't UI.
It's about managing the cognitive load of your own Neovim setup, so nothing gets lost.

English concept

Lightweight, keyboard-friendly TUI-style menus for Neovim (menubar + context menus)

  • Menubar — a title strip at the top of the editor with a dropdown that opens below the selected title
  • Context menus — floating popups positioned at the cursor (normal and visual mode)
  • Nested submenus (multi-level)
  • Per-item conditions (conditions) and filetype filters (ft)
  • Fully configurable keybindings
  • Global keybindings are suppressed inside quickui buffers by default (configurable via suppress_all_keys)
  • Fuzzy finder integration (Telescope, fzf-lua, snacks, mini.pick)

This plugin provides similar functionality to skywind3000/vim-quickui and nvzone/menu. Both were a great source of inspiration, and I'm grateful to their authors.


Requirements

  • Neovim 0.10+

Demo

Menubar

Menubar with nested submenus:

Menubar demo

Context Menu

Context menu at cursor position:

Context menu demo

🧠 Why quickui.nvim?

Modern Neovim setups often include dozens of plugins, but their commands and keybindings are fragmented and hard to manage.

quickui.nvim provides a structured UI — not for recalling or searching, but for organizing your tools by meaning.

Search-based workflows work well when you already remember what exists. But for infrequent actions, what matters more is having a structure you can navigate.

Do you have plugins that seemed useful when you installed them, but ended up unused because you couldn’t remember how to use them?

quickui.nvim makes those features accessible again by turning them into something you can navigate.

It complements the traditional “memorize commands” workflow by adding a new approach: organizing capabilities.

As a result, your menus can act as a portable UI layer — staying consistent even when switching between setups like LazyVim, NvChad, or your own config.

Installation

{
  "mjmjm0101/quickui.nvim",
  lazy = false,
  config = function()
    require("quickui").setup({})
  end,
}
use {
  "mjmjm0101/quickui.nvim",
  config = function()
    require("quickui").setup({})
  end,
}
Plug 'mjmjm0101/quickui.nvim'

lua require("quickui").setup({})

Quick Start

require("quickui").setup({
  keymap = "<Space>",  -- toggle the menubar
  border = "single",

  menus = {
    {
      name  = "&File",
      items = {
        { name = "&New",   cmd = ":enew<CR>", key = "<C-n>" },
        { name = "&Open",  cmd = ":e ",       key = "<C-o>" },
        { name = "&Save",  cmd = ":w<CR>",    key = "<C-s>" },
        { name = "separator" },
        { name = "&Quit",  cmd = ":qa<CR>",   key = "<C-q>", rtxt = "Ctrl-q" },
      },
    },
    {
      name  = "&Edit",
      items = {
        { name = "&Undo",  cmd = "u",      key = "<C-z>" },
        { name = "&Redo",  cmd = "<C-r>",  key = "<C-y>", rtxt = "Ctrl-y" },
        { name = "&Copy",  cmd = '"+y',    key = "<C-c>" },
        { name = "&Paste", cmd = '"+p',    key = "<C-v>" },
      },
    },
  },
})

Or try the sample

require("quickui-sample")

The sample includes:

  • context/normal.lua — normal-mode context menu (LSP, diagnostics, edit, filetype-specific items)
  • context/visual.lua — visual-mode context menu (case conversion, clipboard, indent, sort, LSP range)
  • context/snacks_explorer.lua — context menu for snacks.nvim explorer (open, new, rename, delete, copy path); see the file header for setup instructions

The sample is only a starting point.
quickui.nvim becomes truly useful when you organize your own setup in your own structure — not someone else's.


Configuration Reference

require("quickui").setup({

  -- Key to toggle the menubar (default: "<Space>")
  keymap = "<Space>",

  -- Border style: "none" | "single" | "double" | "rounded" | "dotted" | "dashed"
  -- Note: "none" hides border characters but still reserves their cell width
  -- to prevent layout shifts. Zero-thickness borders are not supported.
  border = "single",

  -- Transparency 0-100. number = both bar and menu, table = individual
  -- Defaults: bar = 0, menu = 40
  winblend = { bar = 0, menu = 40 },

  -- Number of padding spaces on each side of a menubar item (default: 1)
  menubar_padding = 2,

  -- Separator character between menubar items. "" to disable (default: "│")
  menubar_separator = "",

  -- Scroll indicator shown when menus overflow the screen width (defaults: "<" / ">")
  menubar_indicator_left  = "<",
  menubar_indicator_right = ">",

  -- Icon displayed at the right edge of a submenu item (default: "›")
  submenu_icon = "",

  -- Highlight group overrides
  highlights = {
    accent            = "#89b4fa",  -- shortcut letter (character after &)
    rtxt              = "#a6e3a1",  -- right-aligned text (rtxt field)
    menu              = { bg = "#1e1e2e", fg = "#cdd6f4" },
    menu_sel          = { bg = "#4974aa", fg = "#cdd6f4" },
    menu_border       = { fg = "#89b4fa" },
    menubar           = { bg = "#181825", fg = "#cdd6f4" },
    menubar_sel       = { bg = "#313244", fg = "#89b4fa" },
    menubar_separator = { fg = "#585b70", bg = "#181825" },
    menubar_indicator = { fg = "#f38ba8", bold = true },
  },

  -- Keymap overrides (merged on top of defaults)
  keymaps = {
    up        = { "k", "<Up>" },
    down      = { "j", "<Down>" },
    exec      = { "<CR>" },           -- execute item (opens submenu if item has one)
    close     = { "<Esc>", "q" },
    submenu   = { "<Tab>" },             -- open submenu
    back      = { "<BS>", "<S-Tab>" },   -- close submenu and return to parent
    menu_prev = { "h" },                 -- menubar: move to previous menu
    menu_next = { "l" },                 -- menubar: move to next menu
    nav_prev  = { "<Left>" },            -- context-sensitive: close submenu or prev menu
    nav_next  = { "<Right>" },           -- context-sensitive: open submenu or next menu
    mouse     = { "<LeftMouse>" },
  },

  -- If true, start from an empty keymap set (only user-defined keys are active)
  disable_default_keymaps = false,

  -- When true (default), map all keys to <Nop> in plugin buffers to block
  -- global keymaps. Set to false to leave global keymaps untouched.
  suppress_all_keys = true,

  -- When true (default), remember the last open top-level menu and scroll
  -- position and restore them when the menubar is reopened.
  -- When false, always start from the leftmost menu.
  menubar_restore = true,

  -- List of top-level menu specs (see Menu Definition below)
  menus = { ... },
})

Default Keybindings

Action Keys
Move up k / <Up>
Move down j / <Down>
Execute <CR>
Close <Esc> / q
Open submenu <Tab>
Close submenu / back to parent <BS> / <S-Tab>
Menubar: previous menu h
Menubar: next menu l
Close submenu / prev menu <Left>
Open submenu / next menu <Right>
Mouse click <LeftMouse>
Shortcut key (&) Character after & in name
Shortcut key (key) Value of the key item field

Menu Definition

Top-level menu spec

Pass a list of specs to setup() via menus, or register dynamically with require("quickui").menu_install().

{
  name       = "&File",        -- & marks the shortcut character
  priority   = 100,            -- display order, ascending left-to-right (default: 100)
  conditions = function(opt)   -- nil = always shown, false = hidden
    return true
  end,
  items = { ... },             -- item list, or function(opt) → list
}

Names prefixed with &@ or @ default to priority = 10000 and are sorted to the far right.

Item fields

items = {
  -- key only: "<C-s>" is shown as the right-aligned hint and triggers the item
  { name = "&Save",  cmd = ":w<CR>",  key = "<C-s>" },

  -- rtxt overrides key display (key still works as a binding)
  { name = "&Save As...", cmd = ":saveas ", key = "<C-shift-s>", rtxt = "Ctrl-Shift-S" },

  -- rtxt="" suppresses display; key is still active
  { name = "&Close", cmd = ":bd<CR>", key = "<C-w>", rtxt = "" },

  -- rtxt only: display hint with no in-menu keybinding
  { name = "&Redo",  cmd = "<C-r>",   rtxt = "Ctrl-R" },

  -- Separator
  { name = "separator" },

  -- Submenu
  {
    name  = "&Recent",
    items = {
      { name = "file1.txt", cmd = ":e file1.txt<CR>" },
      { name = "file2.txt", cmd = ":e file2.txt<CR>" },
    },
  },

  -- Function command
  { name = "&Grep", cmd = function(opt)
    vim.ui.input({ prompt = "Pattern: " }, function(input)
      if input then vim.cmd("grep " .. input) end
    end)
  end },

  -- Conditional display
  { name = "Laravel &Artisan", cmd = ":!php artisan",
    conditions = function(opt)
      return vim.fn.filereadable("artisan") == 1
    end },

  -- Filetype filter (comma-separated)
  { name = "Validate &HTML", cmd = ":!tidy -errors %",
    ft = "html,xml" },

  -- Per-item highlight
  { name = "Danger Zone", cmd = "...", hl = "ErrorMsg" },
}
Field Type Description
name string Display name. &X sets the shortcut key. %{expr} is evaluated at open time.
cmd string | function Command to run. Strings are fed via feedkeys; functions receive opt.
key string Keybinding active while the menu is open (e.g. "<C-s>"). Also used as the right-aligned hint text when rtxt is not specified.
rtxt string Right-aligned text. Overrides key display when specified. Set to "" to suppress display even if key is set. Supports %{expr} evaluation (same as name).
items table Sub-item list — presence makes this item a submenu trigger.
conditions bool | function false hides the item. Function receives opt, return false to hide.
ft string Comma-separated filetypes. Item is hidden when the current ft doesn't match.
hl string Highlight group applied to the item row.

The opt table passed to cmd and conditions functions:

Key Type Always present Description
filetype string vim.bo.filetype at the time the menu was opened
cwd string vim.fn.getcwd() at the time the menu was opened
selection table|nil context_visual only Visual selection: { text, lines, mode }
…data any when provided All fields from the data argument of context_normal / context_visual

API

require("quickui").setup(opts)

Initialize the plugin. See the Configuration Reference above for all options.

require("quickui").menu_install(spec)

Register or replace a top-level menu at runtime. An existing menu with the same name is replaced.

require("quickui").menu_install({
  name  = "&Debug",
  items = { ... },
})

require("quickui").context_normal(spec, data)

Open a context menu at the cursor position (normal mode).

-- Plain item list
require("quickui").context_normal({
  { name = "Copy",  cmd = '"+y' },
  { name = "Paste", cmd = '"+p' },
})

-- items as a function (dynamic generation)
require("quickui").context_normal({
  items = function(opt)
    return {
      { name = "Filetype: " .. opt.filetype, cmd = "" },
    }
  end,
})

-- Pass arbitrary data through to cmd functions
require("quickui").context_normal(ctx, { target = some_item })
-- cmd = function(opt)  →  opt.target == some_item

require("quickui").context_visual(spec, data)

Open a context menu at the cursor position (visual mode). The visual selection is highlighted while the menu is open. opt.selection is set automatically.

require("quickui").context_visual({
  items = function(opt)
    return {
      { name = "Selection: " .. opt.selection.text, cmd = "" },
      { name = "&Uppercase", cmd = function(opt)
          -- opt.selection.text  — selected text (joined with \n)
          -- opt.selection.lines — table of selected lines
          -- opt.selection.mode  — "v" | "V" | "\22" (block)
        end },
    }
  end,
})

Context Menu Example

-- lua/plugins/quickui.lua
local quickui = require("quickui")

local ctx = {
  items = function(opt)
    return {
      { name = "&Format",      cmd = function() vim.lsp.buf.format() end },
      { name = "&Code Action", cmd = function() vim.lsp.buf.code_action() end },
      { name = "separator" },
      { name = "&Yank All",    cmd = ":%y+<CR>" },
      -- shown only from context_visual
      -- shown only from context_visual (gv re-enters the last visual selection)
      { name = "&Uppercase",   cmd = "gvU",
        conditions = function(opt) return opt.selection ~= nil end },
      { name = "&Lowercase",   cmd = "gvu",
        conditions = function(opt) return opt.selection ~= nil end },
    }
  end,
}

-- Normal mode
vim.keymap.set("n", "<Tab>", function()
  quickui.context_normal(ctx)
end, { noremap = true, silent = true })

-- Visual mode (selection is highlighted; opt.selection is set automatically)
vim.keymap.set("x", "<Tab>", function()
  quickui.context_visual(ctx)
end, { noremap = true, silent = true })

Highlight Groups

Group Default link Target
QuickUIMenubar StatusLine Menubar background
QuickUIMenubarSel PmenuSel Selected menubar item
QuickUIMenubarSeparator NonText Menubar separator character
QuickUIMenubarIndicator WarningMsg Scroll indicator (< / >)
QuickUIMenu Normal Dropdown / popup background
QuickUIMenuBorder FloatBorder Window border
QuickUIMenuSel PmenuSel Selected item row
QuickUIMenuAccent Special Shortcut character (after &)
QuickUIMenuRtxt Special Right-aligned text (rtxt field)
QuickUIVisualSel Visual Visual selection overlay (context_visual)

Fuzzy Finder Integration

quickui.nvim menus can be exposed as searchable data, making it possible to use fuzzy finders as an entry point to your structured UI.

Instead of relying only on remembering commands or guessing search terms, you can search within your own curated structure.

Search results reflect your menu hierarchy and naming, so you can find actions based on how you think about them — not how plugins name them.

License

MIT