Alaja — Declarative CLI framework & terminal rendering kit for Elixir

Copy Markdown View Source

Hex version License Version

Alaja is a declarative CLI framework and terminal rendering kit for Elixir. Define commands with a DSL, validate flags, auto-generate help, and render rich terminal output — tables, headers, boxes, bars, breadcrumbs, JSON syntax highlighting, gradients, and interactive prompts — all powered by true-color ANSI escape sequences.

Alaja is the rendering and I/O layer for the Zaguan toolchain. It depends on Pote for colour management, theme resolution, and format conversions.


Quick Start

Add alaja and pote to your mix.exs:

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

Define a CLI in 5 minutes

defmodule MyApp.CLI do
  use Alaja.CLI.Definition, otp_app: :my_app

  command "deploy", "Deploy to production" 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("Deploying v#{opts.version} to #{opts.env}...")
      if opts.force, do: Alaja.print_warning("Force mode enabled!")
    end
  end

  command "status", "Show system status" do
    run fn _opts ->
      Alaja.Components.Table.print(
        headers: ["Service", "Status", "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

Run it:

mix run -e 'MyApp.CLI.main(["deploy", "1.2.3"])'
mix run -e 'MyApp.CLI.main(["deploy", "1.2.3", "--env", "production", "--force"])'
mix run -e 'MyApp.CLI.main(["status"])'

Rendering Layer

Message printing (12 severity levels)

Alaja.print_success("Deploy completed!")      # ✓ green
Alaja.print_error("Connection refused")        # ✗ red bold
Alaja.print_warning("Disk usage above 80%")    # ⚠ yellow
Alaja.print_info("Processing 12 files...")     # ℹ cyan
Alaja.print_debug("PID: 0.1234.5")             # ⚙ purple
Alaja.print_notice("Maintenance at 02:00")     # 📢 blue
Alaja.print_alert("CPU spike detected!")       # 🔔 inverted warning
Alaja.print_critical("Database unreachable!")  # 🔥 inverted error
Alaja.print_emergency("System crash!")         # 🆘 blinking
Alaja.print_happy("All tests passed!")         # ✨
Alaja.print_sad("Build failed again...")       # ❄

# Dynamic dispatch
Alaja.Printer.print_message(:success, "Done!")
Alaja.Printer.print_message(:error, "Oops!")
FunctionIconStyle
print_success/1,2Green
print_error/1,2Red bold
print_warning/1,2Yellow
print_info/1,2Cyan
print_debug/1,2Purple
print_notice/1,2📢Blue
print_alert/1,2🔔Inverted warn
print_critical/1,2🔥Inverted error
print_emergency/1,2🆘Blinking
print_happy/1,2Happy theme
print_sad/1,2Sad theme
print_message/2Dynamic level

All functions accept printer options: raw: true, x:, y:, align:, verbose:, padding:.

Interactive input

alias Alaja.Printer.Interactive

name   = Interactive.question("What's your name?")
answer = Interactive.yesno("Continue?", default: :no)
result = Interactive.question_with_options("Pick:", [{"Yes", :yes}, {"No", :no}])
Interactive.menu("Select action:", [{"Deploy", :deploy}, {"Rollback", :rollback}])

Printer API (low-level)

# Structured message printing with chunks
chunks = [
  Alaja.Structures.ChunkText.new(" Error: ", color: :error, effects: [:bold]),
  Alaja.Structures.ChunkText.new("File not found", color: :white)
]
msg = Alaja.Structures.MessageInfo.new(chunks, align: :center, padding: 2)
Alaja.Printer.print(msg)

# Raw positioning
Alaja.Printer.print("Loading...", raw: true, x: 10, y: 5)

# Verbose mode returns ANSI string
ansi = Alaja.Printer.print("Hello", verbose: true)

Structures

StructureModulePurpose
ChunkTextAlaja.Structures.ChunkTextText fragment + color + effects
EffectInfoAlaja.Structures.EffectInfoBold, italic, blink, etc.
MessageInfoAlaja.Structures.MessageInfoCompound message + layout opts
chunk = Alaja.Structures.ChunkText.new("Hello", color: "#FF0000", effects: [:bold, :underline])
effects = Alaja.Structures.EffectInfo.new([:bold, :italic, :blink])
msg = Alaja.Structures.MessageInfo.new([chunk], align: :center, padding: 4)

CLI Framework

DSL (Alaja.CLI.Definition)

The declarative DSL provides command, subcommand, flag, argument, and run macros:

defmodule MyApp.CLI do
  use Alaja.CLI.Definition, otp_app: :my_app

  command "build", "Build the project" 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("Building #{opts.target} for #{opts.arch}...")
    end
  end

  subcommand "config", "Manage configuration" do
    command "get", "Read a value" 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", "Write a value" 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

Flag types: :string, :integer, :float, :boolean, :atom.

Global options (Alaja.CLI.GlobalOpts)

12 flags shared by all commands, extracted automatically before command dispatch:

FlagShortTypeDescription
--help-hbooleanShow help
--raw-rbooleanRaw ANSI positioning
--pos-xintegerX coordinate (with --raw)
--pos-yintegerY coordinate (with --raw)
--align-aleft/center/rightText alignment
--verbose-vbooleanReturn ANSI string
--boxbooleanWrap output in a bordered box
--box-titlestringBox title
--box-borderatomBorder style: rounded, double...
--box-colorcolorBorder color
--quiet-qbooleanSuppress output
--stdin-sbooleanRead JSON from stdin

Help system (Alaja.CLI.Help)

Auto-generated help with summary, full reference, and per-command help — all rendered with Alaja's own table and header components.

Validation (Alaja.CLI.Validator)

# Flag type checking
Alaja.CLI.Validator.validate_flags([%{name: :port, type: :integer, required: true}],
                                    [port: "abc"])
# => {:error, ["--port: expected integer, got 'abc'"]}

# Allowed values
Alaja.CLI.Validator.validate_flags([%{name: :env, values: ~w(staging prod)}],
                                    [env: "dev"])
# => {:error, ["--env: 'dev' is not valid. Allowed: staging, prod"]}

# Missing required args
Alaja.CLI.Validator.validate_args([%{name: :version, required: true}], [])
# => {:error, ["Missing required argument: version"]}

# Dangerous command detection
Alaja.CLI.Validator.dangerous?("rm -rf /")
# => true

Error handling (Alaja.CLI.ErrorHandler)

Formatted error messages with "did you mean?" suggestions using Jaro distance, plus proper exit codes:

$ mycli deploi
Error: unknown command 'deploi'

Did you mean?
  deploy

Available commands:
  deploy              Deploy to production
  status              Show system status

Parser utilities (Alaja.CLI.Parser)

# Collect repeated flags
Alaja.CLI.Parser.collect_repeated(~w(--cmd ls --cmd pwd), "--cmd")
# => ["ls", "pwd"]

# Parse colors
Alaja.CLI.Parser.parse_color("#FF0000")
# => {:ok, {255, 0, 0}}

# Parse color lists
Alaja.CLI.Parser.parse_color_list("#FF0000; #00FF00; #0000FF")
# => {:ok, [{255, 0, 0}, {0, 255, 0}, {0, 0, 255}]}

# Parse KEY=VALUE pairs
Alaja.CLI.Parser.parse_env_pair("PATH=/usr/bin")
# => {:PATH, "/usr/bin"}

# Parse alignment
Alaja.CLI.Parser.parse_align("center")
# => :center

Built-in commands reference

Alaja.CLI.Commands.Show — 16 output subcommands:

SubcommandDescription
successSuccess message with green checkmark
errorError message with red cross
warningWarning message with yellow triangle
infoInfo message with cyan indicator
debugDebug message with purple indicator
noticeNotice message with blue indicator
criticalCritical message with magenta indicator
alertAlert message with red indicator
emergencyEmergency message with blinking indicator
happyHappy message with green indicator
sadSad message with blue indicator
messageCustom formatted message (chunks, colors, effects)
tableRich tables with borders, per-cell styling
jsonPretty-printed JSON with syntax highlighting
barProgress bar with customizable appearance
animated-barAnimated progress bar
headerStyled header with optional subtitle
separatorHorizontal divider line with optional text
gradientGradient-colored text (multi-color support)
breadcrumbsNavigation path display
boxBordered container with optional title
animateAnimated spinners and indicators
imageRender images (kitty/iterm2/sixel/ASCII)
listStyled list with optional header
askInteractive text input
menuInteractive selection menu
yesnoInteractive yes/no question

Alaja.CLI.Commands.Config — Configuration management:

ActionDescription
initInitialize ~/.config/alaja
get KEYRead a configuration value
set KEY VALUEWrite a configuration value
theme listList available themes
theme set NAMEActivate a theme
--showPrint current configuration

Visual Components

ModuleDescription
Alaja.Components.TableBordered tables, per-cell/col/row formatting
Alaja.Components.HeaderCentered title + subtitle, 3 sizes
Alaja.Components.SeparatorHorizontal rules with optional centered label
Alaja.Components.BarStatic progress bars, RGB gradients
Alaja.Components.AnimatedBarGenServer-based animated bars (8 styles)
Alaja.Components.BreadcrumbsPath navigation with customizable separator
Alaja.Components.BoxBordered containers (5 border styles)
Alaja.Components.JsonPretty-printed JSON with syntax highlighting
Alaja.Components.ColorWheelHSL wheel, harmony rings, swatches, gradients
Alaja.Components.GradientHorizontal colour ramps via ColorWheel

Examples

Table — per-column formatting, specific row styling, centered:

Alaja.Components.Table.print(
  headers: ["Service", "Status", "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("Hello, world!", title: "Greeting", border: :rounded)
# ╭─ Greeting ──────╮
# │ Hello, world!   │
# ╰─────────────────╯

Bar:

Alaja.Components.Bar.print(75, 100, label: "Upload", width: 40)
Alaja.Components.Bar.print(60, 100, filled_color: {72, 187, 120}, empty_color: {40, 40, 40})

AnimatedBar (8 styles):

{:ok, pid} = Alaja.Components.AnimatedBar.start_link(animation: "moon", length: 30)

# Styles: spinner, kitt, dots, bar, moon, clock, pulse, pulsing_bar

Breadcrumbs:

Alaja.Components.Breadcrumbs.print(["Home", "Projects", "Zaguan"])
# Home › Projects › Zaguan

JSON:

Alaja.Components.Json.print(%{name: "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}])

Available harmonies: triad, complementary, analogous, square, monochromatic, compound, split-complementary.

Image rendering — Kitty, iTerm2, Sixel, or ASCII fallback:

Alaja.ImageRenderer.render_file("logo.png", width: 40, height: 20)
protocol = Alaja.ImageRenderer.detect_protocol()

Raw mode

Print at exact terminal positions:

Alaja.Printer.print("Header", raw: true, x: 0, y: 0, color: :cyan, effects: [:bold])
Alaja.Printer.print("Body text", raw: true, x: 0, y: 2)

# Globally via the command line
# mycli status --raw --pos-x 10 --pos-y 5

Gradients

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"])

Syntax highlighting

# Highlight a file (auto-detects language)
cells = Alaja.Syntax.highlight_file("lib/my_app.ex")

# Highlight content directly
cells = Alaja.Syntax.highlight_content(code, :elixir)

# Tokenize a line
tokens = Alaja.Syntax.tokenize("defmodule Foo do", :elixir)

Supported languages: :elixir, :json, :markdown, :text.


Low-level Modules

ModulePurpose
Alaja.ANSIPure ANSI escape generators (fg, bg, cursor, mouse)
Alaja.TerminalTerminal size detection ({cols, rows})
Alaja.Buffer2D cell grid with flat tuple, O(1) access
Alaja.CellAtomic unit: char + fg/bg RGB + effects list
Alaja.HelpersSparklines, progress bars, boxes, color lerp
Alaja.SyntaxSyntax highlighting for Elixir, JSON, Markdown
Alaja.ImageRendererTerminal image rendering (Kitty/iTerm2/Sixel/ASCII)
Alaja.ImageTerminalImage protocol detection

ANSI escapes:

Alaja.ANSI.fg(0, 180, 216)           # true-color foreground
Alaja.ANSI.bg(40, 44, 52)            # true-color background
Alaja.ANSI.move_to(10, 5)            # cursor to (col, row)
Alaja.ANSI.hide_cursor()
Alaja.ANSI.alt_screen_on()           # alternate buffer
Alaja.ANSI.mouse_on()                # SGR mouse tracking

Buffer + Cell engine:

buffer = Alaja.Buffer.new(80, 24)
buffer = Alaja.Buffer.put(buffer, 10, 5, "X", {255, 0, 0})
cell = Alaja.Buffer.get(buffer, 10, 5)
Alaja.Buffer.write(buffer)  # flush to 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, "Stats", {180, 130, 80})

Configuration

# Key-value store backed by Application env
Alaja.Config.get(:color_depth)           # => :truecolor
Alaja.Config.set(:color_depth, :xterm256)
Alaja.Config.all()                       # all current values

# Theme management
Alaja.Config.list_themes()               # => ["default", "dracula", "monokai", ...]
{:ok, data} = Alaja.Config.load_theme("dracula")

# Built-in themes: default, dracula, monokai, nord, light

Configurable keys: color_depth, theme_active, refresh_rate, double_buffer, max_workers, default_policy.


Dependencies

PackagePurpose
PoteColour management, theme resolution, format conversions
JasonJSON serialization

Dev/tooling:

PackagePurpose
CredoCode linting
DialyxirStatic type analysis
ExDocDocumentation generation
ExCoverallsTest coverage
BatamantaRelease packaging
BencheeBenchmarking

Installation

Add alaja and pote to your mix.exs:

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

Then run mix deps.get.


License

MIT — see LICENSE for details.