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.
Add alaja and pote to your mix.exs:
def deps do
[
{:alaja, path: "../alaja"},
{:pote, path: "../pote"}
]
enddefmodule 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
endRun 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"])'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!")| Function | Icon | Style |
|---|---|---|
print_success/1,2 |
✓ | Green |
print_error/1,2 |
✗ | Red bold |
print_warning/1,2 |
⚠ | Yellow |
print_info/1,2 |
ℹ | Cyan |
print_debug/1,2 |
⚙ | Purple |
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,2 |
✨ | Happy theme |
print_sad/1,2 |
❄ | Sad theme |
print_message/2 |
— | Dynamic level |
All functions accept printer options: raw: true, x:, y:, align:,
verbose:, padding:.
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}])# 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)| Structure | Module | Purpose |
|---|---|---|
ChunkText |
Alaja.Structures.ChunkText |
Text fragment + color + effects |
EffectInfo |
Alaja.Structures.EffectInfo |
Bold, italic, blink, etc. |
MessageInfo |
Alaja.Structures.MessageInfo |
Compound 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)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
endFlag types: :string, :integer, :float, :boolean, :atom.
12 flags shared by all commands, extracted automatically before command dispatch:
| Flag | Short | Type | Description |
|---|---|---|---|
--help |
-h |
boolean | Show help |
--raw |
-r |
boolean | Raw ANSI positioning |
--pos-x |
integer | X coordinate (with --raw) |
|
--pos-y |
integer | Y coordinate (with --raw) |
|
--align |
-a |
left/center/right |
Text alignment |
--verbose |
-v |
boolean | Return ANSI string |
--box |
boolean | Wrap output in a bordered box | |
--box-title |
string | Box title | |
--box-border |
atom | Border style: rounded, double... |
|
--box-color |
color | Border color | |
--quiet |
-q |
boolean | Suppress output |
--stdin |
-s |
boolean | Read JSON from stdin |
Auto-generated help with summary, full reference, and per-command help — all rendered with Alaja's own table and header components.
# 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 /")
# => trueFormatted 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# 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")
# => :centerAlaja.CLI.Commands.Show — 16 output subcommands:
| Subcommand | Description |
|---|---|
success |
Success message with green checkmark |
error |
Error message with red cross |
warning |
Warning message with yellow triangle |
info |
Info message with cyan indicator |
debug |
Debug message with purple indicator |
notice |
Notice message with blue indicator |
critical |
Critical message with magenta indicator |
alert |
Alert message with red indicator |
emergency |
Emergency message with blinking indicator |
happy |
Happy message with green indicator |
sad |
Sad message with blue indicator |
message |
Custom formatted message (chunks, colors, effects) |
table |
Rich tables with borders, per-cell styling |
json |
Pretty-printed JSON with syntax highlighting |
bar |
Progress bar with customizable appearance |
animated-bar |
Animated progress bar |
header |
Styled header with optional subtitle |
separator |
Horizontal divider line with optional text |
gradient |
Gradient-colored text (multi-color support) |
breadcrumbs |
Navigation path display |
box |
Bordered container with optional title |
animate |
Animated spinners and indicators |
image |
Render images (kitty/iterm2/sixel/ASCII) |
list |
Styled list with optional header |
ask |
Interactive text input |
menu |
Interactive selection menu |
yesno |
Interactive yes/no question |
Alaja.CLI.Commands.Config — Configuration management:
| Action | Description |
|---|---|
init |
Initialize ~/.config/alaja |
get KEY |
Read a configuration value |
set KEY VALUE |
Write a configuration value |
theme list |
List available themes |
theme set NAME |
Activate a theme |
--show |
Print current configuration |
| Module | Description |
|---|---|
Alaja.Components.Table |
Bordered tables, per-cell/col/row formatting |
Alaja.Components.Header |
Centered title + subtitle, 3 sizes |
Alaja.Components.Separator |
Horizontal rules with optional centered label |
Alaja.Components.Bar |
Static progress bars, RGB gradients |
Alaja.Components.AnimatedBar |
GenServer-based animated bars (8 styles) |
Alaja.Components.Breadcrumbs |
Path navigation with customizable separator |
Alaja.Components.Box |
Bordered containers (5 border styles) |
Alaja.Components.Json |
Pretty-printed JSON with syntax highlighting |
Alaja.Components.ColorWheel |
HSL wheel, harmony rings, swatches, gradients |
Alaja.Components.Gradient |
Horizontal colour ramps via ColorWheel |
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_barBreadcrumbs:
Alaja.Components.Breadcrumbs.print(["Home", "Projects", "Zaguan"])
# Home › Projects › ZaguanJSON:
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()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 5Alaja.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"])# 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.
| Module | Purpose |
|---|---|
Alaja.ANSI |
Pure ANSI escape generators (fg, bg, cursor, mouse) |
Alaja.Terminal |
Terminal size detection ({cols, rows}) |
Alaja.Buffer |
2D cell grid with flat tuple, O(1) access |
Alaja.Cell |
Atomic unit: char + fg/bg RGB + effects list |
Alaja.Helpers |
Sparklines, progress bars, boxes, color lerp |
Alaja.Syntax |
Syntax highlighting for Elixir, JSON, Markdown |
Alaja.ImageRenderer |
Terminal image rendering (Kitty/iTerm2/Sixel/ASCII) |
Alaja.ImageTerminal |
Image 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 trackingBuffer + 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 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, "Stats", {180, 130, 80})# 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, lightConfigurable keys: color_depth, theme_active, refresh_rate,
double_buffer, max_workers, default_policy.
| Package | Purpose |
|---|---|
| Pote | Colour management, theme resolution, format conversions |
| Jason | JSON serialization |
Dev/tooling:
| Package | Purpose |
|---|---|
| Credo | Code linting |
| Dialyxir | Static type analysis |
| ExDoc | Documentation generation |
| ExCoveralls | Test coverage |
| Batamanta | Release packaging |
| Benchee | Benchmarking |
Add alaja and pote to your mix.exs:
def deps do
[
{:alaja, path: "../alaja"},
{:pote, path: "../pote"}
]
endThen run mix deps.get.
MIT — see LICENSE for details.