wandres.dev
HIGHLIGHTS · el sistema de color

Color en tu plugin: integrarse sin imponerse

Cómo definir grupos propios que respeten el tema del usuario: nombres con prefijo, enlaces marcados como valor por defecto, reaplicación en el evento ColorScheme y namespaces con alcance de ventana.

⏱ 16 min

Un plugin que colorea bien es invisible: se ve como si el tema del usuario lo hubiera contemplado desde siempre, aunque el autor del tema no sepa que tu plugin existe. Un plugin que colorea mal grita, incrusta hexadecimales que chocan con todo y obliga a cada usuario a escribir parches en su configuración. La diferencia entre ambos no está en el gusto del autor, sino en cuatro decisiones técnicas que se toman en menos de veinte líneas.

🎯 Al terminar esta lección sabrás
  • Nombrar grupos propios de forma que no colisionen ni sorprendan.
  • Declarar enlaces con default para ceder siempre ante el usuario.
  • Reaplicar la definición al cambiar de esquema con el evento ColorScheme.
  • Aislar color con namespaces y con la opción winhighlight.

Nombra y enlaza

La primera decisión es el nombre. Tus grupos deben llevar el prefijo del plugin en notación de mayúsculas iniciales, sin guiones ni espacios: MiPluginTitulo, MiPluginRuta, MiPluginSeleccion. El prefijo evita colisiones con los cientos de nombres que ya circulan y, sobre todo, convierte tus grupos en algo documentable: el usuario puede buscarlos y redefinirlos porque forman una familia reconocible.

La segunda decisión es no elegir colores. Cada grupo tuyo debe enlazar a una raíz canónica cuyo significado se parezca al tuyo:

local grupos = {
  MiPluginTitulo    = "Title",
  MiPluginRuta      = "Directory",
  MiPluginSeleccion = "PmenuSel",
  MiPluginTenue     = "Comment",
  MiPluginAviso     = "WarningMsg",
  MiPluginBorde     = "FloatBorder",
}

Esa tabla es todo tu diseño visual. No contiene un solo valor hexadecimal, y sin embargo tu plugin se verá coherente en cualquiera de los miles de temas existentes, incluidos los que se publiquen dentro de cinco años. La elección concreta importa: enlazar a Comment significa “esto es secundario”, enlazar a Title significa “esto encabeza”. Estás heredando semántica, no color.

⚠️
El hexadecimal incrustado es una fuga de abstracción

En cuanto escribes fg = "#89b4fa" en tu plugin, has asumido que el fondo del usuario es oscuro, que su paleta admite ese azul y que ninguna de las dos cosas cambiará. Las tres suposiciones fallan a la vez el día que alguien usa un tema de fondo claro, y el resultado no es feo: es ilegible. Si de verdad necesitas un color literal, condiciónalo al valor de background y ofrece siempre un enlace como salida.

default: cede ante el usuario

Declarar los grupos sin más los impondría por encima de lo que el usuario o su tema hayan dicho. El modificador que estudiaste en la lección 15.2 resuelve el problema en una palabra:

local function aplicar()
  for nombre, destino in pairs(grupos) do
    vim.api.nvim_set_hl(0, nombre, { link = destino, default = true })
  end
end

Con default en cierto, cada enlace se aplica solo si el grupo no estaba ya definido. Si el tema del usuario conoce tu plugin y colorea MiPluginTitulo a su manera, tu declaración se descarta en silencio y gana el tema. Si nadie ha dicho nada, tu valor razonable entra en juego. Nunca hay conflicto, nunca hay que documentar un orden de carga, nunca hay que pedirle al usuario que llame a una función de reparación.

La regla general que se deriva merece grabarse: un plugin propone, un tema dispone, el usuario decide. Tu código pertenece al primer escalón y no debe intentar subir.

flowchart TD
A[El plugin declara sus grupos con default] --> B[El grupo ya estaba definido]
B -->|si| C[Gana la definicion previa del tema o del usuario]
B -->|no| D[Entra el enlace por defecto del plugin]
C --> E[Se dibuja]
D --> E
F[El usuario cambia de esquema] --> G[Evento ColorScheme]
G --> A
style A fill:#89b4fa,color:#11111b
style E fill:#a6e3a1,color:#11111b

Reaplicar en ColorScheme

Falta una pieza de ciclo de vida. Cambiar de esquema limpia la tabla de grupos, de modo que tus definiciones desaparecen si solo las aplicaste al cargar el plugin. El usuario ve tus ventanas perder el color y culpa a tu plugin, con razón.

La solución es reaccionar al evento correspondiente, en un grupo de autocomandos propio para que recargar el plugin no acumule manejadores duplicados:

local ag = vim.api.nvim_create_augroup("MiPluginColor", { clear = true })

vim.api.nvim_create_autocmd("ColorScheme", {
  group = ag,
  callback = aplicar,
})

aplicar() -- y también ahora, para el esquema ya activo

La llamada final no es redundante: el evento solo se dispara en cambios futuros, y tu plugin puede cargarse mucho después de que el esquema se haya aplicado. Declarar el manejador y ejecutar una vez es el patrón completo.

🏷️

Prefijo estable

Trata los nombres de tus grupos como parte de tu API pública. Renombrar MiPluginTitulo rompe las configuraciones de tus usuarios exactamente igual que renombrar una función exportada.

📖

Documentación

Publica la tabla de grupos y sus enlaces por defecto en tu ayuda. Un usuario que no sabe qué nombres existe no puede personalizarlos, y acabará escribiendo parches frágiles basados en prueba y error.

🧪

Verificación

En tus pruebas, consulta con nvim_get_hl resolviendo la cadena y comprueba que ningún grupo tuyo queda sin definición efectiva bajo un tema mínimo.

🌗

Degradación

Si añades algún atributo literal, aporta también su equivalente para terminal indexada. Un plugin que solo se ve con color de veinticuatro bits excluye a quien trabaja en consolas limitadas.

Namespaces y winhighlight

A veces no quieres cambiar el color de un nombre en todo el editor, sino solo dentro de tu ventana. Hay dos mecanismos, y elegir el adecuado evita mucho daño colateral.

El primero es la opción winhighlight, que reasigna nombres dentro de una ventana concreta. Es la herramienta correcta para que tu ventana flotante use un fondo distinto sin tocar el Normal global:

vim.api.nvim_set_option_value(
  "winhighlight",
  "Normal:MiPluginFondo,FloatBorder:MiPluginBorde",
  { win = win }
)

El segundo son los namespaces, que ya asomaron en la lección 15.1 como primer argumento de nvim_set_hl. Un namespace es una tabla de grupos alternativa que puedes activar en una ventana entera:

local ns = vim.api.nvim_create_namespace("MiPlugin")
vim.api.nvim_set_hl(ns, "Normal", { fg = "#cdd6f4", bg = "#181825" })
vim.api.nvim_win_set_hl_ns(win, ns)

El criterio de elección es simple. Si solo necesitas redirigir unos pocos nombres, winhighlight es más barato y más legible. Si vas a redefinir muchos grupos con coherencia interna —una interfaz completa con su propio aspecto—, el namespace expresa mejor la intención y evita una cadena interminable de reasignaciones. En ambos casos el resto del editor queda intacto, que es exactamente el contrato que un buen plugin debe cumplir.

Advertencia sobre el alcance: incluso dentro de un namespace, la disciplina anterior sigue vigente. Los grupos a los que rediriges deben ser tuyos y estar enlazados con default, no colores literales. Aislar el alcance te protege de romper el editor del usuario, pero no te dispensa de respetar su tema dentro de tu propia ventana.

Y queda un caso frecuente que merece nombre propio, el de los grupos compartidos con el ecosistema. Si tu plugin muestra diagnósticos, resultados de búsqueda o elementos de menú, no inventes nombres: reutiliza los que ya existen. Un panel de resultados que usa Search para las coincidencias se integra sin esfuerzo con cualquier tema, mientras que uno que define MiPluginCoincidencia obliga a cada usuario a configurarlo. Crea nombres propios solo para conceptos que de verdad son tuyos.

Ceder el control es la característica

Todo lo anterior converge en una idea que va más allá del color: tu plugin no es dueño de la apariencia del editor, es invitado en la de otro. El modificador default, los nombres con prefijo, la reaplicación en el evento y el aislamiento por ventana son cuatro formas del mismo gesto, el de dejar puntos de extensión abiertos en lugar de decisiones cerradas. Y aquí está lo contraintuitivo: el plugin que cede el control es el que acaba viéndose mejor, porque hereda gratis el trabajo de diseño de miles de temas que jamás podrías igualar tú solo. El que incrusta su propia paleta se ve bien exactamente en el entorno de su autor y mal en todos los demás. La lección se generaliza a cualquier cosa que publiques: cada valor que fijas es una integración que impides, y en un ecosistema donde nadie coordina versiones ni calendarios, la interoperabilidad no se consigue acordando nada, sino renunciando a decidir lo que no te toca.

⚔️ Un plugin que respeta el tema
  1. Define una tabla con cuatro grupos propios, todos con prefijo y todos enlazados a raíces canónicas.
  2. Aplícalos con default en cierto y comprueba que redefinir uno antes en tu configuración gana la partida.
  3. Registra el manejador de ColorScheme en un grupo de autocomandos con limpieza, y llama una vez al aplicar.
  4. Cambia de esquema tres veces seguidas y verifica que tus grupos siguen resolviendo a una definición válida.
  5. Abre una ventana flotante y dale fondo propio, primero con winhighlight y después con un namespace. Compara qué mecanismo expresa mejor la intención en tu caso.