Alaja — Framework CLI declarativo y kit de renderizado para terminal en Elixir
Copy Markdown View Source 
Alaja es un framework CLI declarativo y kit de renderizado para terminal en Elixir. Define comandos con un DSL, valida flags, genera ayuda automática y renderiza salida de terminal enriquecida — tablas, headers, cajas, barras, breadcrumbs, Wizard, multibar, resaltado de sintaxis JSON, degradados y prompts interactivos — todo con secuencias ANSI de color verdadero (24-bit).
A partir de la versión 2.0.0, los componentes son Buffer-first: retornan
Alaja.Buffer.t() en lugar de iodata o strings, permitiendo composición
avanzada (overlays, apilados, recortes).
Alaja es un framework CLI declarativo y kit de renderizado para terminal, publicado como librería independiente. Depende de Pote para la gestión de colores, resolución de temas y conversiones de formato.
Inicio rápido
Agrega alaja y pote a tu mix.exs:
def deps do
[
{:alaja, github: "Lorenzo-SF/alaja"}
{:pote, github: "Lorenzo-SF/pote"}
]
endhalt_on_error (v0.3.3+)
Por defecto, Alaja.CLI.Definition NO llama a System.halt/1 en errores
de despacho (comando desconocido, argumento faltante, flag inválido).
Esto hace que el DSL sea seguro de usar como librería — llamar a main/1
desde un test o desde otro módulo no mata el BEAM.
Para comportamiento estilo escript (exit code 1 en error), opt-in explícito:
use Alaja.CLI.Definition, otp_app: :my_app, halt_on_error: trueCuando halt_on_error: false (el default), main/1 retorna
{:error, reason} donde reason es uno de :no_command,
:unknown_command, :no_handler, :invalid_command, etc.
Define un CLI en 5 minutos
defmodule MiApp.CLI do
use Alaja.CLI.Definition, otp_app: :mi_app
command "deploy", "Despliega a producción" do
flag :env, :string, default: "staging", values: ~w(staging production)
flag :force, :boolean, default: false
argument :version, :string, required: true
run fn opts ->
Alaja.print_success("Desplegando v#{opts.version} en #{opts.env}...")
if opts.force, do: Alaja.print_warning("¡Modo forzado activado!")
end
end
command "status", "Muestra el estado del sistema" do
run fn _opts ->
Alaja.Components.Table.print(
headers: ["Servicio", "Estado", "Uptime"],
rows: [
["api", "OK", "12d 4h"],
["db", "OK", "30d 2h"],
["cache", "WARN", "2h 15m"]
],
table_border: :rounded,
rows_2_color: [:white, :yellow, :white]
)
end
end
endEjecútalo:
mix run -e 'MiApp.CLI.main(["deploy", "1.2.3"])'
mix run -e 'MiApp.CLI.main(["deploy", "1.2.3", "--env", "production", "--force"])'
mix run -e 'MiApp.CLI.main(["status"])'
Capa de renderizado
Impresión de mensajes (12 niveles de severidad)
Alaja.print_success("¡Deploy completado!") # ✓ verde
Alaja.print_error("Conexión rechazada") # ✗ rojo negrita
Alaja.print_warning("Uso de disco > 80%") # ⚠ amarillo
Alaja.print_info("Procesando 12 archivos...") # ℹ cyan
Alaja.print_debug("PID: 0.1234.5") # ⚙ púrpura
Alaja.print_notice("Mantenimiento a las 02:00") # 📢 azul
Alaja.print_alert("¡Pico de CPU detectado!") # 🔔 warning invertido
Alaja.print_critical("¡Base de datos caída!") # 🔥 error invertido
Alaja.print_emergency("¡Fallo del sistema!") # 🆘 parpadeante
Alaja.print_happy("¡Todos los tests pasaron!") # ✨
Alaja.print_sad("Build falló otra vez...") # ❄
# Despacho dinámico
Alaja.Printer.print_message(:success, "¡Hecho!")
Alaja.Printer.print_message(:error, "¡Ups!")| Función | Icono | Estilo |
|---|---|---|
print_success/1,2 | ✓ | Verde |
print_error/1,2 | ✗ | Rojo negrita |
print_warning/1,2 | ⚠ | Amarillo |
print_info/1,2 | ℹ | Cyan |
print_debug/1,2 | ⚙ | Púrpura |
print_notice/1,2 | 📢 | Azul |
print_alert/1,2 | 🔔 | Warning invertido |
print_critical/1,2 | 🔥 | Error invertido |
print_emergency/1,2 | 🆘 | Parpadeante |
print_happy/1,2 | ✨ | Tema happy |
print_sad/1,2 | ❄ | Tema sad |
print_message/2 | — | Nivel dinámico |
Todas las funciones aceptan opciones de impresión: raw: true, x:, y:,
align:, verbose:, padding:.
Entrada interactiva
alias Alaja.Printer.Interactive
nombre = Interactive.question("¿Cómo te llamas?")
resp = Interactive.yesno("¿Continuar?", default: :no)
result = Interactive.question_with_options("Elige:", [{"Sí", :si}, {"No", :no}])
Interactive.menu("Selecciona acción:", [{"Deploy", :deploy}, {"Rollback", :rollback}])API de impresión (bajo nivel)
# Impresión estructurada con chunks
chunks = [
Alaja.Structures.ChunkText.new(" Error: ", color: :error, effects: [:bold]),
Alaja.Structures.ChunkText.new("Archivo no encontrado", color: :white)
]
msg = Alaja.Structures.MessageInfo.new(chunks, align: :center, padding: 2)
Alaja.Printer.print(msg)
# Posicionamiento raw
Alaja.Printer.print("Cargando...", raw: true, x: 10, y: 5)
# Modo verbose devuelve string ANSI
ansi = Alaja.Printer.print("Hola", verbose: true)Estructuras
| Estructura | Módulo | Propósito |
|---|---|---|
ChunkText | Alaja.Structures.ChunkText | Fragmento de texto + color + efec |
EffectInfo | Alaja.Structures.EffectInfo | Negrita, itálica, parpadeo, etc. |
MessageInfo | Alaja.Structures.MessageInfo | Mensaje compuesto + layout |
chunk = Alaja.Structures.ChunkText.new("Hola", color: "#FF0000", effects: [:bold, :underline])
efectos = Alaja.Structures.EffectInfo.new([:bold, :italic, :blink])
msg = Alaja.Structures.MessageInfo.new([chunk], align: :center, padding: 4)Framework CLI
DSL (Alaja.CLI.Definition)
El DSL declarativo provee las macros command, subcommand, flag,
argument y run:
defmodule MiApp.CLI do
use Alaja.CLI.Definition, otp_app: :mi_app
command "build", "Compila el proyecto" do
flag :release, :boolean, default: false
flag :arch, :string, default: "amd64", values: ~w(amd64 arm64)
argument :target, :string, required: true
run fn opts ->
IO.puts("Compilando #{opts.target} para #{opts.arch}...")
end
end
subcommand "config", "Gestiona configuración" do
command "get", "Lee un valor" do
argument :key, :string, required: true
run fn opts ->
value = Alaja.Config.get(String.to_atom(opts.key))
IO.puts("#{opts.key}: #{inspect(value)}")
end
end
command "set", "Escribe un valor" do
argument :key, :string, required: true
argument :value, :string, required: true
run fn opts ->
Alaja.Config.set(String.to_atom(opts.key), opts.value)
Alaja.print_success("#{opts.key} = #{opts.value}")
end
end
end
endTipos de flag: :string, :integer, :float, :boolean, :atom.
Opciones globales (Alaja.CLI.GlobalOpts)
12 flags compartidos por todos los comandos, extraídos automáticamente antes del dispatch:
| Flag | Corto | Tipo | Descripción |
|---|---|---|---|
--help | -h | boolean | Mostrar ayuda |
--raw | -r | boolean | Posicionamiento ANSI raw |
--pos-x | integer | Coordenada X (con --raw) | |
--pos-y | integer | Coordenada Y (con --raw) | |
--align | -a | left/center/right | Alineación del texto |
--verbose | -v | boolean | Devolver string ANSI |
--box | boolean | Envolver salida en caja | |
--box-title | string | Título de la caja | |
--box-border | atom | Estilo: rounded, double... | |
--box-color | color | Color del borde | |
--quiet | -q | boolean | Suprimir salida |
--stdin | -s | boolean | Leer JSON de stdin |
Sistema de ayuda (Alaja.CLI.Help)
Ayuda generada automáticamente con resumen, referencia completa y ayuda por comando — todo renderizado con los propios componentes de tabla y header de Alaja.
Validación (Alaja.CLI.Validator)
# Verificación de tipos de flags
Alaja.CLI.Validator.validate_flags([%{name: :port, type: :integer, required: true}],
[port: "abc"])
# => {:error, ["--port: expected integer, got 'abc'"]}
# Valores permitidos
Alaja.CLI.Validator.validate_flags([%{name: :env, values: ~w(staging prod)}],
[env: "dev"])
# => {:error, ["--env: 'dev' is not valid. Allowed: staging, prod"]}
# Argumentos requeridos faltantes
Alaja.CLI.Validator.validate_args([%{name: :version, required: true}], [])
# => {:error, ["Missing required argument: version"]}
# Detección de comandos peligrosos
Alaja.CLI.Validator.dangerous?("rm -rf /")
# => trueManejo de errores (Alaja.CLI.ErrorHandler)
Mensajes de error formateados con sugerencias "did you mean?" usando distancia Jaro, y códigos de salida apropiados:
$ micli deploi
Error: unknown command 'deploi'
Did you mean?
deploy
Available commands:
deploy Despliega a producción
status Muestra el estado del sistema
Utilidades de parseo (Alaja.CLI.Parser)
# Recolectar flags repetidos
Alaja.CLI.Parser.collect_repeated(~w(--cmd ls --cmd pwd), "--cmd")
# => ["ls", "pwd"]
# Parsear colores
Alaja.CLI.Parser.parse_color("#FF0000")
# => {:ok, {255, 0, 0}}
# Parsear listas de colores
Alaja.CLI.Parser.parse_color_list("#FF0000; #00FF00; #0000FF")
# => {:ok, [{255, 0, 0}, {0, 255, 0}, {0, 0, 255}]}
# Parsear pares CLAVE=VALOR
Alaja.CLI.Parser.parse_env_pair("PATH=/usr/bin")
# => {:PATH, "/usr/bin"}
# Parsear alineación
Alaja.CLI.Parser.parse_align("center")
# => :centerReferencia de comandos integrados
Alaja.CLI.Commands.Show — 16 subcomandos de salida:
| Subcomando | Descripción |
|---|---|
success | Mensaje de éxito con checkmark verde |
error | Mensaje de error con cruz roja |
warning | Mensaje de advertencia con triángulo amarillo |
info | Mensaje informativo con indicador cyan |
debug | Mensaje de debug con indicador púrpura |
notice | Mensaje de aviso con indicador azul |
critical | Mensaje crítico con indicador magenta |
alert | Mensaje de alerta con indicador rojo |
emergency | Mensaje de emergencia con indicador parpadeante |
happy | Mensaje positivo con indicador verde |
sad | Mensaje negativo con indicador azul |
message | Mensaje formateado (chunks, colores, efectos) |
table | Tablas enriquecidas con bordes y estilos por celda |
json | JSON formateado con resaltado de sintaxis |
bar | Barra de progreso con apariencia personalizable |
animated-bar | Barra de progreso animada |
header | Header estilizado con subtítulo opcional |
separator | Línea divisoria horizontal con texto opcional |
gradient | Texto con degradado de color (soporte multi-color) |
breadcrumbs | Ruta de navegación |
box | Contenedor con bordes y título opcional |
animate | Spinners e indicadores animados |
image | Renderizado de imágenes (kitty/iterm2/sixel/ASCII) |
list | Lista estilizada con header opcional |
ask | Entrada de texto interactiva |
menu | Menú de selección interactivo |
multibar | Rastreador multi-tarea con barras paralelas |
yesno | Pregunta interactiva sí/no |
Alaja.CLI.Commands.Config — Gestión de configuración:
| Acción | Descripción |
|---|---|
init | Inicializar ~/.config/alaja |
get CLAVE | Leer un valor de configuración |
set CLAVE VALOR | Escribir un valor de configuración |
theme list | Listar temas disponibles |
theme set NOMBRE | Activar un tema |
--show | Mostrar configuración actual |
Componentes visuales
| Módulo | Descripción |
|---|---|
Alaja.Components.Table | Tablas con bordes, formato por celda/col/fila |
Alaja.Components.Header | Título centrado + subtítulo, 3 tamaños |
Alaja.Components.Separator | Líneas horizontales con etiqueta centrada opcional |
Alaja.Components.Bar | Barras de progreso estáticas, degradados RGB |
Alaja.Components.AnimatedBar | Barras animadas con GenServer (8 estilos) |
Alaja.Components.MultiBar | Barra multi-tarea con GenServer |
Alaja.Components.Breadcrumbs | Navegación tipo ruta con separador personalizable |
Alaja.Components.Box | Contenedores con bordes (5 estilos) |
Alaja.Components.Json | JSON formateado con resaltado de sintaxis |
Alaja.Components.ColorWheel | Rueda HSL, anillos de armonía, muestras, degradados |
Alaja.Components.Gradient | Rampas de color horizontales via ColorWheel |
Ejemplos
Table — formato por columna, estilos por fila específica, centrada:
Alaja.Components.Table.print(
headers: ["Servicio", "Estado", "Uptime"],
rows: [
["api", "OK", "12d"],
["db", "OK", "30d"],
["cache", "WARN", "2h"]
],
headers_color: :cyan,
headers_effects: [:bold],
rows_2_color: [:white, :yellow, :white],
table_border: :rounded,
table_align: :center
)Box:
Alaja.Components.Box.print("¡Hola, mundo!", title: "Saludo", border: :rounded)
# ╭─ Saludo ──────────╮
# │ ¡Hola, mundo! │
# ╰───────────────────╯Bar:
Alaja.Components.Bar.print(75, 100, label: "Subida", width: 40)
Alaja.Components.Bar.print(60, 100, filled_color: {72, 187, 120}, empty_color: {40, 40, 40})AnimatedBar (8 estilos):
{:ok, pid} = Alaja.Components.AnimatedBar.start_link(animation: "moon", length: 30)
# Estilos: spinner, kitt, dots, bar, moon, clock, pulse, pulsing_barBreadcrumbs:
Alaja.Components.Breadcrumbs.print(["Inicio", "Proyectos", "Zaguan"])
# Inicio › Proyectos › ZaguanJSON:
Alaja.Components.Json.print(%{nombre: "Alaja", version: "2.0.0", deps: ["pote", "jason"]})ColorWheel:
Alaja.Components.ColorWheel.show_color_info({255, 87, 51})
Alaja.Components.ColorWheel.show_harmony_ring({255, 0, 0}, :triad)
Alaja.Components.ColorWheel.show_swatches([{255, 0, 0}, {0, 255, 0}, {0, 0, 255}])Armonías disponibles: triad, complementary, analogous, square,
monochromatic, compound, split-complementary.
Renderizado de imágenes — Kitty, iTerm2, Sixel o fallback ASCII:
Alaja.ImageRenderer.render_file("logo.png", width: 40, height: 20)
protocolo = Alaja.ImageRenderer.detect_protocol()Modo raw
Imprime en posiciones exactas de la terminal:
Alaja.Printer.print("Cabecera", raw: true, x: 0, y: 0, color: :cyan, effects: [:bold])
Alaja.Printer.print("Texto", raw: true, x: 0, y: 2)
# Globalmente desde línea de comandos
# micli status --raw --pos-x 10 --pos-y 5Degradados
Alaja.Helpers.progress_bar(75, 20, {80, 140, 255}, {200, 100, 255})
Alaja.Helpers.lerp({255, 0, 0}, {0, 0, 255}, 0.5) # => {127, 0, 127}
Alaja.Components.ColorWheel.show_gradient(["#FF0000", "#00FF00", "#0000FF"])Resaltado de sintaxis
# Resaltar un archivo (autodetección de lenguaje)
celdas = Alaja.Syntax.highlight_file("lib/mi_app.ex")
# Resaltar contenido directamente
celdas = Alaja.Syntax.highlight_content(codigo, :elixir)
# Tokenizar una línea
tokens = Alaja.Syntax.tokenize("defmodule Foo do", :elixir)Lenguajes soportados: :elixir, :json, :markdown, :text.
Módulos de bajo nivel
| Módulo | Propósito |
|---|---|
Alaja.ANSI | Generadores puros de escape ANSI (fg, bg, cursor, mouse) |
Alaja.Terminal | Detección de tamaño de terminal ({cols, rows}) |
Alaja.Buffer | Cuadrícula 2D con tupla plana, acceso O(1) |
Alaja.Cell | Unidad atómica: char + fg/bg RGB + lista de efectos |
Alaja.Helpers | Sparklines, barras, cajas, interpolación de color |
Alaja.Syntax | Resaltado de sintaxis para Elixir, JSON, Markdown |
Alaja.ImageRenderer | Renderizado de imágenes (Kitty/iTerm2/Sixel/ASCII) |
Alaja.ImageTerminal | Detección de protocolos de imagen |
Escapes ANSI:
Alaja.ANSI.fg(0, 180, 216) # color verdadero foreground
Alaja.ANSI.bg(40, 44, 52) # color verdadero background
Alaja.ANSI.move_to(10, 5) # cursor a (col, row)
Alaja.ANSI.hide_cursor()
Alaja.ANSI.alt_screen_on() # buffer alternativo
Alaja.ANSI.mouse_on() # tracking de mouse SGRMotor Buffer + Cell:
buffer = Alaja.Buffer.new(80, 24)
buffer = Alaja.Buffer.put(buffer, 10, 5, "X", {255, 0, 0})
celda = Alaja.Buffer.get(buffer, 10, 5)
Alaja.Buffer.write(buffer) # volcar a stdoutHelpers:
Alaja.Helpers.braille_spark([10, 50, 90, 30, 70], 5)
Alaja.Helpers.box(1, 1, 40, 10, "Workers", {100, 140, 200})
Alaja.Helpers.double_box(1, 1, 40, 10, "Estadísticas", {180, 130, 80})Configuración
# Almacén clave-valor sobre Application env
Alaja.Config.get(:color_depth) # => :truecolor
Alaja.Config.set(:color_depth, :xterm256)
Alaja.Config.all() # todos los valores actuales
# Gestión de temas
Alaja.Config.list_themes() # => ["default", "dracula", "monokai", ...]
{:ok, datos} = Alaja.Config.load_theme("dracula")
# Temas incluidos: default, dracula, monokai, nord, lightClaves configurables: color_depth, theme_active, refresh_rate,
double_buffer, max_workers, default_policy.
Dependencias
| Paquete | Propósito |
|---|---|
| Pote | Gestión de color, resolución de temas, conversiones de formato |
| Jason | Serialización JSON |
Dev/herramientas:
| Paquete | Propósito |
|---|---|
| Credo | Linting de código |
| Dialyxir | Análisis estático de tipos |
| ExDoc | Generación de documentación |
| ExCoveralls | Cobertura de tests |
| Batamanta | Empaquetado de releases |
| Benchee | Benchmarking |
Motor Cell/Buffer (v0.3.0+)
Los componentes devuelven Alaja.Buffer.t() (una cuadrícula 2D de
Alaja.Cell.t()) desde su función render/N en lugar de iodata opaco.
Esto desbloquea composición — overlays, apilados horizontal/vertical,
recortes, padding — todo sin re-implementar cada componente.
buf1 = Components.Header.render("Estado", color: :cyan)
buf2 = Components.Bar.render(width: 30, percent: 75, color: :green)
# Superponer buf2 en la esquina inferior derecha de buf1
combined = Buffer.overlay(buf1, buf2,
offset_x: 5,
offset_y: Buffer.height(buf1) - Buffer.height(buf2) - 1
)
# Imprimir con posicionamiento de cursor
Printer.print_buffer(combined)Primitivas de composición disponibles en Alaja.Buffer:
to_iodata/1— fusiona celdas consecutivas con el mismo estado ANSI en un único prefijo de escape + caracteres (como cualquier emulador de terminal). Crítico para paridad visual con la salida pre-v0.3.0.overlay/4— coloca un buffer encima de otro en(offset_x, offset_y).hstack/2— concatena horizontalmente (bordes superiores alineados).vstack/2— concatena verticalmente (bordes izquierdos alineados).crop/5— extrae un sub-rectángulo.pad/3— añade padding (top/right/bottom/left, fg, bg).with_offset/3— traslada un buffer por (dx, dy).
Componentes movidos al motor Cell en v0.3.0:
| Componente | Función | Retorna |
|---|---|---|
| Separator | Components.Separator.render/2 | Buffer.t() |
| Bar | Components.Bar.render/2 | Buffer.t() |
| Breadcrumbs | Components.Breadcrumbs.render/2 | Buffer.t() |
| Header | Components.Header.render/2 | Buffer.t() |
| Box | Components.Box.render/2 | Buffer.t() |
| Json | Components.Json.render/2 | Buffer.t() |
| Table | Components.Table.render_buffer/2 | Buffer.t() |
| Pulsar | (ya era Cell) | Buffer.t() |
El Components.Table.render/2 legacy mantiene salida iodata para
compatibilidad con consumidores anteriores.
Instalación
Agrega alaja y pote a tu mix.exs:
def deps do
[
{:alaja, "~> 2.0"},
{:pote, github: "Lorenzo-SF/pote", branch: "main"}
]
endLuego ejecuta mix deps.get.
Nota sobre el historial
El historial de git de este repositorio fue reseteado como parte de un esfuerzo deliberado de limpieza. Los commits que puedes leer aquí describen el codebase tal como existe hoy — no preservan la cronología original del desarrollo.
Cualquier cosa que valía la pena mantener del período anterior al reset fue traspasada como releases etiquetados con entradas explícitas en el
CHANGELOG.md. Cualquier cosa no preservada es, por decisión del responsable, ya no parte de la línea de desarrollo canónica.
Licencia
MIT — consulta LICENSE para más detalles.