Alaja — Framework CLI declarativo y kit de renderizado para terminal en Elixir

Copy Markdown View Source

Hex version Version License

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, resaltado de sintaxis JSON, degradados y prompts interactivos — todo con secuencias ANSI de color verdadero (24-bit).

Alaja es la capa de renderizado e I/O para el toolchain Zaguan. 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, path: "../alaja"},
    {:pote, path: "../pote"}
  ]
end

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
end

Ejecú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ónIconoEstilo
print_success/1,2Verde
print_error/1,2Rojo negrita
print_warning/1,2Amarillo
print_info/1,2Cyan
print_debug/1,2Pú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,2Tema happy
print_sad/1,2Tema sad
print_message/2Nivel 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

EstructuraMóduloPropósito
ChunkTextAlaja.Structures.ChunkTextFragmento de texto + color + efec
EffectInfoAlaja.Structures.EffectInfoNegrita, itálica, parpadeo, etc.
MessageInfoAlaja.Structures.MessageInfoMensaje 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
end

Tipos 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:

FlagCortoTipoDescripción
--help-hbooleanMostrar ayuda
--raw-rbooleanPosicionamiento ANSI raw
--pos-xintegerCoordenada X (con --raw)
--pos-yintegerCoordenada Y (con --raw)
--align-aleft/center/rightAlineación del texto
--verbose-vbooleanDevolver string ANSI
--boxbooleanEnvolver salida en caja
--box-titlestringTítulo de la caja
--box-borderatomEstilo: rounded, double...
--box-colorcolorColor del borde
--quiet-qbooleanSuprimir salida
--stdin-sbooleanLeer 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 /")
# => true

Manejo 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")
# => :center

Referencia de comandos integrados

Alaja.CLI.Commands.Show — 16 subcomandos de salida:

SubcomandoDescripción
successMensaje de éxito con checkmark verde
errorMensaje de error con cruz roja
warningMensaje de advertencia con triángulo amarillo
infoMensaje informativo con indicador cyan
debugMensaje de debug con indicador púrpura
noticeMensaje de aviso con indicador azul
criticalMensaje crítico con indicador magenta
alertMensaje de alerta con indicador rojo
emergencyMensaje de emergencia con indicador parpadeante
happyMensaje positivo con indicador verde
sadMensaje negativo con indicador azul
messageMensaje formateado (chunks, colores, efectos)
tableTablas enriquecidas con bordes y estilos por celda
jsonJSON formateado con resaltado de sintaxis
barBarra de progreso con apariencia personalizable
animated-barBarra de progreso animada
headerHeader estilizado con subtítulo opcional
separatorLínea divisoria horizontal con texto opcional
gradientTexto con degradado de color (soporte multi-color)
breadcrumbsRuta de navegación
boxContenedor con bordes y título opcional
animateSpinners e indicadores animados
imageRenderizado de imágenes (kitty/iterm2/sixel/ASCII)
listLista estilizada con header opcional
askEntrada de texto interactiva
menuMenú de selección interactivo
yesnoPregunta interactiva sí/no

Alaja.CLI.Commands.Config — Gestión de configuración:

AcciónDescripción
initInicializar ~/.config/alaja
get CLAVELeer un valor de configuración
set CLAVE VALOREscribir un valor de configuración
theme listListar temas disponibles
theme set NOMBREActivar un tema
--showMostrar configuración actual

Componentes visuales

MóduloDescripción
Alaja.Components.TableTablas con bordes, formato por celda/col/fila
Alaja.Components.HeaderTítulo centrado + subtítulo, 3 tamaños
Alaja.Components.SeparatorLíneas horizontales con etiqueta centrada opcional
Alaja.Components.BarBarras de progreso estáticas, degradados RGB
Alaja.Components.AnimatedBarBarras animadas con GenServer (8 estilos)
Alaja.Components.BreadcrumbsNavegación tipo ruta con separador personalizable
Alaja.Components.BoxContenedores con bordes (5 estilos)
Alaja.Components.JsonJSON formateado con resaltado de sintaxis
Alaja.Components.ColorWheelRueda HSL, anillos de armonía, muestras, degradados
Alaja.Components.GradientRampas 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_bar

Breadcrumbs:

Alaja.Components.Breadcrumbs.print(["Inicio", "Proyectos", "Zaguan"])
# Inicio › Proyectos › Zaguan

JSON:

Alaja.Components.Json.print(%{nombre: "Zaguan", version: "1.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 5

Degradados

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óduloPropósito
Alaja.ANSIGeneradores puros de escape ANSI (fg, bg, cursor, mouse)
Alaja.TerminalDetección de tamaño de terminal ({cols, rows})
Alaja.BufferCuadrícula 2D con tupla plana, acceso O(1)
Alaja.CellUnidad atómica: char + fg/bg RGB + lista de efectos
Alaja.HelpersSparklines, barras, cajas, interpolación de color
Alaja.SyntaxResaltado de sintaxis para Elixir, JSON, Markdown
Alaja.ImageRendererRenderizado de imágenes (Kitty/iTerm2/Sixel/ASCII)
Alaja.ImageTerminalDetecció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 SGR

Motor 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 stdout

Helpers:

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, light

Claves configurables: color_depth, theme_active, refresh_rate, double_buffer, max_workers, default_policy.


Dependencias

PaquetePropósito
PoteGestión de color, resolución de temas, conversiones de formato
JasonSerialización JSON

Dev/herramientas:

PaquetePropósito
CredoLinting de código
DialyxirAnálisis estático de tipos
ExDocGeneración de documentación
ExCoverallsCobertura de tests
BatamantaEmpaquetado de releases
BencheeBenchmarking

Instalación

Agrega alaja y pote a tu mix.exs:

def deps do
  [
    {:alaja, path: "../alaja"},
    {:pote, path: "../pote"}
  ]
end

Luego ejecuta mix deps.get.


Licencia

MIT — consulta LICENSE para más detalles.