Skip to content

Instantly share code, notes, and snippets.

@eduardoarandah
Created February 12, 2026 13:19
Show Gist options
  • Select an option

  • Save eduardoarandah/ee92a2de714011ef5bebc962157599db to your computer and use it in GitHub Desktop.

Select an option

Save eduardoarandah/ee92a2de714011ef5bebc962157599db to your computer and use it in GitHub Desktop.
neovim plugin to visualize which keys you have free in your keymaps
-- real-estate.nvim
-- Plugin para visualizar qué teclas están ocupadas/libres en tus keymaps
local M = {}
--------------------------------------------------------------------------------
-- LAYOUTS: Distribución de teclas por modo
-- Cada fila es un string, las teclas especiales van entre < >
--------------------------------------------------------------------------------
M.layouts = {
normal = {
"<F1><F2><F3><F4><F5><F6><F7><F8><F9><F10><F11><F12>",
"`1234567890-=",
"qwertyuiop[]\\",
"asdfghjkl;'",
"zxcvbnm,./",
"<Space>",
},
upper = {
"<F13><F14><F15><F16><F17><F18><F19><F20><F21><F22><F23><F24>",
"~!@#$%^&*()_+",
"QWERTYUIOP{}|",
'ASDFGHJKL:"',
"ZXCVBNM<>?",
},
}
--------------------------------------------------------------------------------
-- KEYMAPS DEFAULT DE VIM (built-in, no configurados por el usuario)
-- Cobertura completa de teclas mostradas en el layout
--------------------------------------------------------------------------------
local default_keymaps = {
-- ============ CTRL + TECLA ============
["<C-A>"] = "increment",
["<C-B>"] = "page up",
["<C-C>"] = "cancel/interrupt",
["<C-D>"] = "scroll down ½",
["<C-E>"] = "scroll down 1",
["<C-F>"] = "page down",
["<C-G>"] = "file info",
["<C-H>"] = "backspace",
["<C-I>"] = "jump forward",
["<C-J>"] = "down (like j)",
["<C-K>"] = "digraph",
["<C-L>"] = "redraw",
["<C-M>"] = "enter",
["<C-N>"] = "down/next match",
["<C-O>"] = "jump back",
["<C-P>"] = "up/prev match",
["<C-Q>"] = "visual block",
["<C-R>"] = "redo",
["<C-S>"] = "freeze terminal",
["<C-T>"] = "pop tag",
["<C-U>"] = "scroll up ½",
["<C-V>"] = "visual block",
["<C-W>"] = "window cmd",
["<C-X>"] = "decrement",
["<C-Y>"] = "scroll up 1",
["<C-Z>"] = "suspend",
["<C-]>"] = "jump to tag",
["<C-^>"] = "alt file",
["<C-/>"] = "undo",
-- ============ TECLAS NORMALES (minúsculas) ============
-- Fila de números
["`"] = "goto mark exact",
["1"] = "repeat 1x",
["2"] = "repeat 2x",
["3"] = "repeat 3x",
["4"] = "repeat 4x",
["5"] = "repeat 5x",
["6"] = "repeat 6x",
["7"] = "repeat 7x",
["8"] = "repeat 8x",
["9"] = "repeat 9x",
["0"] = "line start",
["-"] = "line up",
["="] = "(unused)",
-- Fila QWERTY
["q"] = "record macro",
["w"] = "word fwd",
["e"] = "word end",
["r"] = "replace char",
["t"] = "till char",
["y"] = "yank",
["u"] = "undo",
["i"] = "insert mode",
["o"] = "open line below",
["p"] = "paste after",
["["] = "prev section",
["]"] = "next section",
["\\"] = "(unused)",
-- Fila ASDF
["a"] = "append",
["s"] = "subst char",
["d"] = "delete",
["f"] = "find char",
["g"] = "go prefix",
["h"] = "left",
["j"] = "down",
["k"] = "up",
["l"] = "right",
[";"] = "repeat f/t",
["'"] = "goto mark",
-- Fila ZXCV
["z"] = "fold/scroll",
["x"] = "delete char",
["c"] = "change",
["v"] = "visual mode",
["b"] = "word back",
["n"] = "next search",
["m"] = "set mark",
[","] = "repeat f/t back",
["."] = "repeat cmd",
["/"] = "search",
-- Espacio
[" "] = "leader/right",
-- ============ TECLAS MAYÚSCULAS ============
-- Fila de símbolos (shift+números)
["~"] = "toggle case",
["!"] = "filter extern",
["@"] = "execute macro",
["#"] = "search word back",
["$"] = "line end",
["%"] = "match bracket",
["^"] = "first char",
["&"] = "repeat :s",
["*"] = "search word fwd",
["("] = "sentence back",
[")"] = "sentence fwd",
["_"] = "line down 1st",
["+"] = "line down 1st",
-- Fila QWERTY mayúscula
["Q"] = "ex mode",
["W"] = "WORD fwd",
["E"] = "WORD end",
["R"] = "replace mode",
["T"] = "till char back",
["Y"] = "yank line",
["U"] = "undo line",
["I"] = "insert at 1st",
["O"] = "open line above",
["P"] = "paste before",
["{"] = "para back",
["}"] = "para fwd",
["|"] = "goto column",
-- Fila ASDF mayúscula
["A"] = "append at end",
["S"] = "subst line",
["D"] = "delete to end",
["F"] = "find char back",
["G"] = "goto line/EOF",
["H"] = "screen top",
["J"] = "join lines",
["K"] = "keyword lookup",
["L"] = "screen bottom",
[":"] = "command mode",
['"'] = "register",
-- Fila ZXCV mayúscula
["Z"] = "quit prefix",
["X"] = "backspace",
["C"] = "change to end",
["V"] = "visual line",
["B"] = "WORD back",
["N"] = "prev search",
["M"] = "screen middle",
["<"] = "unindent",
[">"] = "indent",
["?"] = "search back",
-- ============ TECLAS FUNCIÓN ============
["<F1>"] = "help",
}
--------------------------------------------------------------------------------
-- ESTADO INTERNO
--------------------------------------------------------------------------------
local state = {
buf = nil, -- buffer del floating window
win = nil, -- window id
mode_index = 1, -- índice del modo actual
keymaps = {}, -- keymaps parseados
}
-- Lista de modos disponibles para ciclar con izquierda/derecha
local modes = { "normal", "upper", "ctrl", "leader_normal", "leader_upper", "localleader_normal", "localleader_upper", "visual_normal", "visual_upper", "insert_normal", "insert_upper" }
--------------------------------------------------------------------------------
-- HIGHLIGHT GROUPS
--------------------------------------------------------------------------------
local function setup_highlights()
-- Tecla ocupada por usuario: rojo
vim.api.nvim_set_hl(0, "RealEstateOccupied", { fg = "#ff5555", bold = true })
-- Tecla default de vim: amarillo
vim.api.nvim_set_hl(0, "RealEstateDefault", { fg = "#f1fa8c" })
-- Tecla libre: gris suave
vim.api.nvim_set_hl(0, "RealEstateFree", { fg = "#6272a4" })
-- Título del modo
vim.api.nvim_set_hl(0, "RealEstateTitle", { fg = "#50fa7b", bold = true })
-- Bordes de tecla
vim.api.nvim_set_hl(0, "RealEstateBracket", { fg = "#44475a" })
end
--------------------------------------------------------------------------------
-- PARSEO DE TECLAS DESDE UN STRING DE LAYOUT
-- Retorna tabla de teclas, ej: {"F1", "F2", "a", "b", ...}
--------------------------------------------------------------------------------
local function parse_layout_row(row)
local keys = {}
local i = 1
while i <= #row do
if row:sub(i, i) == "<" then
-- Tecla especial: buscar el cierre >
local j = row:find(">", i)
if j then
-- Extraer sin los < >
table.insert(keys, row:sub(i + 1, j - 1))
i = j + 1
else
-- Malformado, avanzar
i = i + 1
end
else
-- Tecla normal: un solo carácter
table.insert(keys, row:sub(i, i))
i = i + 1
end
end
return keys
end
--------------------------------------------------------------------------------
-- OBTENER KEYMAPS DE NEOVIM PARA MÚLTIPLES MODOS
-- Retorna tabla por modo: { n = {...}, v = {...}, i = {...} }
--------------------------------------------------------------------------------
local function get_keymaps()
local result = { n = {}, v = {}, i = {} }
-- Obtener keymaps de cada modo vim
for vim_mode, _ in pairs(result) do
local maps = vim.api.nvim_get_keymap(vim_mode)
for _, map in ipairs(maps) do
local lhs = map.lhs
local desc = map.desc or map.rhs or "[callback]"
result[vim_mode][lhs] = { desc = desc }
end
end
return result
end
--------------------------------------------------------------------------------
-- VERIFICAR ESTADO DE UNA TECLA: user_mapped, default, o free
-- Retorna: { status = "user"|"default"|"free", desc = "descripción o nil" }
--------------------------------------------------------------------------------
local function get_key_status(mode, key, keymaps)
local check_key
local check_key_alt -- Para variantes (ej: ctrl mayúscula/minúscula)
-- Determinar qué tabla de keymaps usar según el modo
local vim_mode = "n" -- default: normal mode keymaps
if mode:match("^visual") then
vim_mode = "v"
elseif mode:match("^insert") then
vim_mode = "i"
end
local mode_keymaps = keymaps[vim_mode] or {}
if mode == "normal" or mode == "upper" or mode == "visual_normal" or mode == "visual_upper" or mode == "insert_normal" or mode == "insert_upper" then
-- Tecla directa
if key == "Space" then
check_key = " "
elseif #key == 1 then
check_key = key
else
check_key = "<" .. key .. ">"
end
elseif mode == "ctrl" then
-- Ctrl + tecla (case insensitive, vim usa mayúscula)
local base = key == "Space" and "Space" or key:upper()
check_key = "<C-" .. base .. ">"
check_key_alt = "<C-" .. key:lower() .. ">"
elseif mode == "leader_normal" then
if #key == 1 then
check_key = " " .. key:lower()
else
check_key = " <" .. key .. ">"
end
elseif mode == "leader_upper" then
if #key == 1 then
check_key = " " .. key:upper()
else
check_key = " <" .. key .. ">"
end
elseif mode == "localleader_normal" then
if #key == 1 then
check_key = "\\" .. key:lower()
else
check_key = "\\<" .. key .. ">"
end
elseif mode == "localleader_upper" then
if #key == 1 then
check_key = "\\" .. key:upper()
else
check_key = "\\<" .. key .. ">"
end
end
-- 1. Primero revisar keymaps del usuario (prioridad)
if mode_keymaps[check_key] then
return { status = "user", desc = mode_keymaps[check_key].desc, lhs = check_key }
end
if check_key_alt and mode_keymaps[check_key_alt] then
return { status = "user", desc = mode_keymaps[check_key_alt].desc, lhs = check_key_alt }
end
-- 2. Revisar defaults de vim (solo para modos normal/ctrl, no insert/visual)
if not mode:match("^insert") then
local default_desc = default_keymaps[check_key]
if default_desc then
return { status = "default", desc = default_desc, lhs = check_key }
end
if check_key_alt then
default_desc = default_keymaps[check_key_alt]
if default_desc then
return { status = "default", desc = default_desc, lhs = check_key_alt }
end
end
-- Para modos normal/upper/visual, también revisar la tecla simple
if mode == "normal" or mode == "upper" or mode == "visual_normal" or mode == "visual_upper" then
default_desc = default_keymaps[key]
if default_desc then
return { status = "default", desc = default_desc, lhs = key }
end
end
end
return { status = "free", desc = nil, lhs = nil }
end
--------------------------------------------------------------------------------
-- RENDERIZAR UNA FILA DE TECLAS CON FORMATO [ x ]
-- Retorna: { line, highlights, user_keys = [{lhs, desc}], default_keys = [{lhs, desc}] }
--------------------------------------------------------------------------------
local function render_row(keys, mode, keymaps)
local parts = {}
local highlights = {}
local user_keys = {} -- Teclas mapeadas por usuario
local default_keys = {} -- Teclas default de vim
local col = 0
for _, key in ipairs(keys) do
-- Mostrar la tecla (máx 5 chars para alinear)
local display = key
if #display > 5 then
display = display:sub(1, 5)
end
-- Padding para centrar en 5 chars
local pad_total = 5 - #display
local pad_left = math.floor(pad_total / 2)
local pad_right = pad_total - pad_left
local padded = string.rep(" ", pad_left) .. display .. string.rep(" ", pad_right)
local cell = "[ " .. padded .. " ]"
table.insert(parts, cell)
-- Obtener estado de la tecla
local key_info = get_key_status(mode, key, keymaps)
local hl_group
if key_info.status == "user" then
hl_group = "RealEstateOccupied"
table.insert(user_keys, { lhs = key_info.lhs or key, desc = key_info.desc })
elseif key_info.status == "default" then
hl_group = "RealEstateDefault"
table.insert(default_keys, { lhs = key_info.lhs or key, desc = key_info.desc })
else
hl_group = "RealEstateFree"
end
-- Posiciones: "[ " = 2 chars, luego 5 chars de tecla, luego " ]" = 2 chars
table.insert(highlights, { col, col + 1, "RealEstateBracket" })
table.insert(highlights, { col + 8, col + 9, "RealEstateBracket" })
table.insert(highlights, { col + 2, col + 7, hl_group })
col = col + #cell + 1
end
return {
line = table.concat(parts, " "),
highlights = highlights,
user_keys = user_keys,
default_keys = default_keys,
}
end
--------------------------------------------------------------------------------
-- OBTENER EL LAYOUT CORRECTO SEGÚN EL MODO
--------------------------------------------------------------------------------
local function get_layout_for_mode(mode)
-- Modos que usan layout normal (minúsculas)
if mode == "normal" or mode == "ctrl" or mode == "leader_normal" or mode == "localleader_normal" or mode == "visual_normal" or mode == "insert_normal" then
return M.layouts.normal
else
-- Modos upper: upper, leader_upper, localleader_upper, visual_upper, insert_upper
return M.layouts.upper
end
end
--------------------------------------------------------------------------------
-- FORMATEAR LISTA DE KEYMAPS EN LÍNEAS COMPACTAS
-- Cada entrada: "lhs → desc", agrupadas hasta max_width
--------------------------------------------------------------------------------
local function format_keymap_list(keymap_list, max_width)
local result = {}
local current_line = " "
for _, km in ipairs(keymap_list) do
-- Truncar descripción si es muy larga
local desc = km.desc or ""
if #desc > 20 then
desc = desc:sub(1, 17) .. "..."
end
local entry = km.lhs .. "→" .. desc
-- Si no cabe en la línea actual, empezar nueva
if #current_line + #entry + 3 > max_width then
if #current_line > 2 then
table.insert(result, current_line)
end
current_line = " " .. entry
else
if #current_line > 2 then
current_line = current_line .. " " .. entry
else
current_line = current_line .. entry
end
end
end
-- Agregar última línea si tiene contenido
if #current_line > 2 then
table.insert(result, current_line)
end
return result
end
--------------------------------------------------------------------------------
-- MOSTRAR EL TECLADO EN UN FLOATING WINDOW
--------------------------------------------------------------------------------
function M.show(mode, keymaps)
local layout = get_layout_for_mode(mode)
local lines = {}
local all_highlights = {}
local all_user_keys = {} -- Todas las teclas de usuario
local all_default_keys = {} -- Todas las teclas default
-- Título del modo
local title = " Mode: " .. mode:upper() .. " "
table.insert(lines, title)
table.insert(lines, "")
-- Renderizar cada fila del layout
for _, row in ipairs(layout) do
local keys = parse_layout_row(row)
local rendered = render_row(keys, mode, keymaps)
table.insert(lines, rendered.line)
table.insert(all_highlights, { line = #lines - 1, hl = rendered.highlights })
-- Acumular keymaps
for _, km in ipairs(rendered.user_keys) do
table.insert(all_user_keys, km)
end
for _, km in ipairs(rendered.default_keys) do
table.insert(all_default_keys, km)
end
end
-- Leyenda de colores
table.insert(lines, "")
table.insert(lines, " ■ user ■ vim default ■ free")
-- Ancho máximo para formatear listas
local list_max_width = 80
-- Mostrar keymaps de usuario si hay
if #all_user_keys > 0 then
table.insert(lines, "")
table.insert(lines, " User mappings:")
local user_lines = format_keymap_list(all_user_keys, list_max_width)
for _, line in ipairs(user_lines) do
table.insert(lines, line)
end
end
-- Mostrar defaults de vim si hay
if #all_default_keys > 0 then
table.insert(lines, "")
table.insert(lines, " Vim defaults:")
local default_lines = format_keymap_list(all_default_keys, list_max_width)
for _, line in ipairs(default_lines) do
table.insert(lines, line)
end
end
-- Línea de ayuda
table.insert(lines, "")
table.insert(lines, " ← / → : cambiar modo q / <Esc> : cerrar")
-- Calcular dimensiones basado en el contenido real
local max_width = 0
for _, line in ipairs(lines) do
local display_width = vim.fn.strdisplaywidth(line)
if display_width > max_width then
max_width = display_width
end
end
local width = max_width + 2
local height = #lines
-- Posición centrada
local ui = vim.api.nvim_list_uis()[1]
local row = math.floor((ui.height - height) / 2)
local col = math.floor((ui.width - width) / 2)
-- Crear buffer si no existe o fue cerrado
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
state.buf = vim.api.nvim_create_buf(false, true)
end
-- Escribir contenido ANTES de crear/redimensionar ventana
vim.api.nvim_buf_set_option(state.buf, "modifiable", true)
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, lines)
vim.api.nvim_buf_set_option(state.buf, "modifiable", false)
-- Crear ventana si no existe o fue cerrada
if not state.win or not vim.api.nvim_win_is_valid(state.win) then
state.win = vim.api.nvim_open_win(state.buf, true, {
relative = "editor",
width = width,
height = height,
row = row,
col = col,
style = "minimal",
border = "rounded",
title = " RealEstate ",
title_pos = "center",
})
else
-- Actualizar tamaño y posición
vim.api.nvim_win_set_config(state.win, {
relative = "editor",
width = width,
height = height,
row = row,
col = col,
})
end
-- Aplicar highlights
local ns = vim.api.nvim_create_namespace("real_estate")
vim.api.nvim_buf_clear_namespace(state.buf, ns, 0, -1)
-- Highlight del título
vim.api.nvim_buf_add_highlight(state.buf, ns, "RealEstateTitle", 0, 0, -1)
-- Encontrar la línea de leyenda y aplicar colores
for i, line in ipairs(lines) do
if line:match("■ user") then
-- Posiciones aproximadas de los cuadrados en la leyenda
local idx = i - 1
local user_pos = line:find("■ user")
local def_pos = line:find("■ vim")
local free_pos = line:find("■ free")
if user_pos then
vim.api.nvim_buf_add_highlight(state.buf, ns, "RealEstateOccupied", idx, user_pos - 1, user_pos + 2)
end
if def_pos then
vim.api.nvim_buf_add_highlight(state.buf, ns, "RealEstateDefault", idx, def_pos - 1, def_pos + 2)
end
if free_pos then
vim.api.nvim_buf_add_highlight(state.buf, ns, "RealEstateFree", idx, free_pos - 1, free_pos + 2)
end
break
end
end
-- Highlights de las teclas
for _, row_hl in ipairs(all_highlights) do
for _, hl in ipairs(row_hl.hl) do
vim.api.nvim_buf_add_highlight(state.buf, ns, hl[3], row_hl.line, hl[1], hl[2])
end
end
-- Keymaps locales del buffer
local opts = { buffer = state.buf, noremap = true, silent = true }
-- Cerrar con q o Escape
vim.keymap.set("n", "q", function()
M.close()
end, opts)
vim.keymap.set("n", "<Esc>", function()
M.close()
end, opts)
-- Cambiar modo con flechas
vim.keymap.set("n", "<Right>", function()
state.mode_index = (state.mode_index % #modes) + 1
M.show(modes[state.mode_index], state.keymaps)
end, opts)
vim.keymap.set("n", "<Left>", function()
state.mode_index = state.mode_index - 1
if state.mode_index < 1 then
state.mode_index = #modes
end
M.show(modes[state.mode_index], state.keymaps)
end, opts)
end
--------------------------------------------------------------------------------
-- CERRAR EL FLOATING WINDOW
--------------------------------------------------------------------------------
function M.close()
if state.win and vim.api.nvim_win_is_valid(state.win) then
vim.api.nvim_win_close(state.win, true)
end
state.win = nil
state.buf = nil
end
--------------------------------------------------------------------------------
-- COMANDO PRINCIPAL :RealEstate
--------------------------------------------------------------------------------
function M.open()
setup_highlights()
state.keymaps = get_keymaps()
state.mode_index = 1
M.show(modes[1], state.keymaps)
end
--------------------------------------------------------------------------------
-- REGISTRAR COMANDOS
--------------------------------------------------------------------------------
function M.register_commands()
vim.api.nvim_create_user_command("RealEstate", function()
M.open()
end, { desc = "Show keyboard layout with occupied/free keys" })
end
--------------------------------------------------------------------------------
-- SETUP: Permite configurar layouts personalizados
--------------------------------------------------------------------------------
function M.setup(opts)
opts = opts or {}
if opts.layouts then
M.layouts = vim.tbl_deep_extend("force", M.layouts, opts.layouts)
end
M.register_commands()
end
return M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment