Current section
Files
Jump to
Current section
Files
CHANGELOG.md
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [2.2.0] - 2026-07-08
### Added
- **`alaja table` per-row styling**: new `--row-N-color`, `--row-N-align`,
`--row-N-effect` CLI flags where N = 1-indexed row number. These override
the global `--rows-color/--rows-align/--rows-effects` for a specific row.
Supports both `--row-1-color VALUE` and `--row-1-color=VALUE` syntax.
Internal row numbers are 0-indexed and mapped automatically.
### Fixed
- **BUG**: `alaja table` per-row styling was only implemented in the backend
(`Alaja.Components.Table`) but had no CLI support. The parser
`extract_row_specific_opts/1` already handled `rows_0_color` etc. but no
CLI switches exposed them. Fixed by adding `parse_per_row_args/1` which
captures `--row-N-{color,align,effect}` from raw args, translates to the
backend's `rows_{N-1}_{color,align,effects}` format, and merges them into
the option list (global applies first, per-row overrides).
- **BUG**: theme atom colours (e.g. `:primary`, `:secondary`) resolved to
Pote's hardcoded `@default_colors` instead of the user's active Alaja
theme. `Cell.ansi_fg/1` and `Cell.ansi_bg/1` called `Pote.color/1` which
only does `Map.get(@default_colors, atom)`, completely bypassing the
registered theme resolver stack. Fixed by replacing with
`Pote.resolve_theme_color/1`, which walks registered resolvers first and
only falls back to `@default_colors` as a last resort. Affects any
component using `Cell` (Separator, Header, Box, Table via Buffer, etc.).
## [2.1.0] - 2026-07-07
### Fixed
- **`source_ref` in `mix.exs`** pointed to a non-existent tag (`v0.1.0`).
Now points to the canonical `2.0.0` tag (matches the rest of the
Lorenzo-SF/* ecosystem after the doc/source_ref sweep).
- **CHANGELOG** — drop the `v` prefix from tag references in the
"A note on history" footer to match the canonical tag convention
(no `v` prefix on this repo).
- **Dep switch**: `pote` is now tracked from `github: "Lorenzo-SF/pote",
branch: "main"` again (was `~> 2.0` on Hex). Aligns alaja with the
rest of the ecosystem (apero, arrea, candil, botica all use
`branch: "main"` for cross-repo deps).
- **`Alaja.SnapshotHelper`**: align the `Table` pipeline with the Cell
Buffer convention (regenerates JSON snapshots after the theme colour
change in 2.0.0).
### Changed
- **JSON test snapshots regenerated** to match the new theme palette
introduced in 2.0.0.
### Internal
- **`mix dialyzer`** config: add `plt_add_apps: [:mix]` so the PLT
contains `Mix.shell/0`, `Mix.Project.project_file/0`, and the
`Mix.Task` callback info. The `alaja.snapshot` mix task references
these and dialyzer was failing with
`Function Mix.shell/0 does not exist` against the previous PLT.
## [2.0.0] - 2026-07-01
This release marks a major API shift: alaja is now **Buffer-first**.
Components that previously returned strings, iodata, or PNG bytes
expose a canonical `Alaja.Buffer.t/0` entry point. PNG/iodata paths
still exist but are now explicit (e.g. `render_for_terminal/2`
returns a tagged tuple) rather than the only option.
This entry consolidates everything between 0.2.0 and the current
HEAD — including the 0.3.0 Cell/Buffer engine unification, the
0.3.x hardening pass, the 2.0.0 Wizard + Syntax + Config work, the
multibar command, and the latest house-keeping. Earlier `0.x` and
`0.2.0` versions are no longer maintained and have been collapsed
into this single canonical entry.
### Added
#### Core rendering
- **Cell/Buffer engine unification.** Components return `Alaja.Buffer.t()`
from their `render/N` entry points (a 2D grid of `Alaja.Cell.t()`)
instead of opaque iodata. Single source of truth for 2D layout.
- **Composition primitives in `Alaja.Buffer`**: `to_iodata/1`
(ANSI-coalesced), `overlay/4`, `hstack/2`, `vstack/2`, `crop/5`,
`pad/3`, `with_offset/3`.
- **`Alaja.Printer.print_buffer/2`** — prints a buffer at `(x, y)`
with cursor positioning, honoring `buffer.offset_x/offset_y`.
- **All `Alaja.Components.*` moved to the Cell engine**: `Separator`,
`Bar`, `Breadcrumbs`, `Header`, `Json` (tokenizer-based), `Box`
(accepts `Buffer.t()` as content), `ColorWheel` (`render/2`,
`render_for_terminal/2`, `default_opts/0`), `Table.render_buffer/2`.
#### CLI framework
- **Rich DSL from hex 1.0.0**: `command/3`, `subcommand`, `flag`,
`argument`, `run {Mod, :fun}`, auto-generated `main/1`. Flag types:
`:string, :integer, :float, :boolean, :atom, :path, :url, :color_list`.
- **`Alaja.CLI.Definition.cast_flag_value/3`** hardened: `:path, :url,
:color_list` clauses added; `:integer/:float` no longer crash on
garbage input — fall back to the flag's default.
- **`Alaja.CLI.Dispatch`** centralises dispatch from `Alaja.CLI` into
the existing `Alaja.CLI.Commands.*` handlers.
- **`Alaja.run/1,2`** — unified entry point. Default `cli_module` is
`Alaja.CLI`; pass any module built with
`use Alaja.CLI.Definition, otp_app: :my_app`.
- **`halt_on_error: false` default** — `main/1` returns `{:error, reason}`
instead of `System.halt(1)`, making the DSL safe as a library.
- **27 commands wired** (covered by `test/alaja/cli/dispatch_test.exs`).
- **`alaja multibar`** — multi-task progress tracking with parallel
bars. Two modes: demo (animated simulation for `--duration` seconds)
and stdin (interactive pipe protocol with `progress`/`success`/
`error`/`wait`/`info`/`done`).
#### New components
- **`Alaja.Wizard`** — declarative multi-field form renderer. Builds
a `Buffer.t/0` via five neutral renderers:
`:inline, :compact, :stacked, :wizard, :compact_wizard`. Pure:
same input, same output. No renderer named after any product,
brand, or AI assistant.
- **`Alaja.Components.MultiBar`** — GenServer-based multi-task
progress bar. Four task states (`:running, :success, :error,
:wait`), per-task progress tracking, dynamic descriptions, and
in-place ANSI repaint via cursor-up.
- **`Alaja.Components.AnimatedBar`** — animated progress bar (8 styles).
- **`Alaja.Components.Json`** — pretty-printed JSON with syntax
highlighting (tokenizer-based, returns `Buffer.t/0`).
#### Syntax highlighting
- **`Alaja.Syntax.highlight_buffer/3`** — canonical Buffer-first entry
point. Per-line tokenization preserves newlines; ANSI-16 palette is
mapped to RGB tuples so Buffer cells carry proper fg colours.
- **Supported languages**: `:elixir, :json, :markdown, :text`.
#### Configuration
- **`Alaja.Config.load!/1,2`** — path-based config loader with
optional `:skip_env` (for deterministic tests). Honours
`ALAJAX_COLOR_DEPTH` and `ALAJAX_THEME_ACTIVE` env vars as an
overlay on top of the on-disk JSON.
- **`Alaja.Theme`** — facade generated by `use Pote.Theme, config_app: :alaja`.
Exposes `list/0, active/0, activate/1, color/1, colors/0, install!/1,
install_template/1, templates/0, register_with_pote/0, storage_dir/0`.
Themes are JSON files under `~/.config/alaja/themes/` (or
`ALAJA_THEMES_PATH` env var). Auto-registers its resolver with `Pote`
on application start so `Pote.parse("theme:<key>")` consults Alaja's
active theme.
- **Five built-in templates** with the full 22-key colour set
(`primary, secondary, ternary, quaternary, success, warning, error,
info, debug, happy, sad, gradient_1..6, menu, alert, critical,
no_color, background`): `default, dracula, monokai, nord, light`.
- **`Alaja.Application`** — registers Pote's theme resolver on boot
so `Pote.parse("theme:<key>")` consults the active theme.
- **`Alaja.Config.lookup_theme_color/1`** — looks up a key in the
active theme (used by Pote's theme resolver bridge).
### Changed
- **`Alaja.Components.Table.render/2`** now returns `Buffer.t/0` for
the canonical entry point. Use `Table.render_iodata/2` for the
legacy iodata path (exotic formatting only).
- **`Alaja.Printer.print_raw/2`** now accepts both `Buffer.t()` and
iodata. The Buffer path uses `Buffer.to_iodata/1` internally and
applies box wrapping at the Buffer level to preserve ANSI
coalescing end-to-end.
- **`Alaja.Components.ColorWheel`** exposes a canonical `render/2`,
`render_for_terminal/2`, and `default_opts/0` API that returns
`Buffer.t/0`. The PNG path is reached through
`render_for_terminal/2`'s `{:image, iodata}` return value.
- **`Alaja.Components.Box.render/2`** now returns a `Buffer.t()` and
accepts a `Buffer.t()` as content (Buffer-in, Buffer-out pipeline).
- **`Alaja.CLI.Definition.cast_flag_value/3` failure mode** for bad
`:integer/:float` input changed from "raise" to "fall back to default".
- **`Alaja.Syntax.Renderer.Theme`** is now aliased into `Alaja.Syntax`
so `Theme.resolve/3` is reachable from the new `highlight_buffer/3`
without a fully-qualified call.
- **i18n**: translated remaining Spanish docstrings and inline comments
to English across the library.
- **Self-hosting**: `Alaja.CLI` now uses `use Alaja.CLI.Definition`
to define its commands (replaces manual `command_dispatch/0` and
`command_descriptions/0`).
- **Dep switch**: `pote` is now pinned to `~> 2.0` on Hex.pm
(was `github: "Lorenzo-SF/pote", branch: "main"`). Required for
`mix hex.build` to succeed.
- **`Alaja.Helpers`** no longer exposes the 11 deprecated ANSI
wrappers (`move, clear, hide_cursor, show_cursor, clear_line, fg, bg,
bold, dim, italic, reset`); internal helpers call `Alaja.ANSI.*`
directly.
- **DSL `run` macro** accepts `{module, function}` tuples instead of
arbitrary anonymous functions.
- **`Pote.Conversions.*`** calls in `Alaja.CLI.Commands.Color` migrated
to `Pote.Converters.Advanced.*` and `Pote.Converters.RGB.*`.
### Fixed
- **BUG**: `alaja color <cualquiersa>` crashed with `ArgumentError: not
an iodata term` in `Printer.print_raw/2`. Root cause:
`Alaja.Components.Table.render/2` was migrated to return `Buffer.t/0`
but the CLI Color command still embedded the Buffer inside an
iolist. Fixed by applying `Buffer.to_iodata/1` in `build_color_analysis/3`
and the extras table block in `lib/alaja/cli/commands/color.ex`
(matches the pattern in `multi_bar.ex` and `show/bar.ex`).
- **BUG**: `Alaja.Components.MultiBar` repaint used fragile DEC SC/RC
(`\e7`/`\e8`); replaced with cursor-up (`\r` + `\e[<line_count-1>A` +
`\e[J`). Old approach caused frames to accumulate on terminals that
don't implement the DEC private save/restore stack reliably (iTerm2,
Terminal.app, Kitty).
- **BUG**: `Alaja.Printer.print_raw/2` crashed with `ArgumentError:
not an iodata term` whenever input was `Buffer.t()` and `:box` was
set. Fixed by applying box wrapping at the Buffer level so ANSI
coalescing is preserved end-to-end.
- **BUG**: `Alaja.CLI.Definition.main/1` used to call `System.halt/1`
on every dispatch error, making the framework unusable as a library.
Now returns `{:error, reason}` unless the consumer opts in via
`halt_on_error: true`.
- **BUG**: `question_with_options/3` only matched the user's input
against the full display label. Now lists options under the prompt
with 1-based indices and matches in order: integer index, exact
label (case-insensitive), label prefix (case-insensitive), atom name,
or `:error`. New `:default` kwarg picks a 1-based default index.
`yesno/2` rewritten on top of it.
- **BUG**: `alaja config theme set <name>` did not change the colour
palette used by `theme:<key>` lookups or atom lookups. `alaja config
init` was writing hand-rolled theme JSON in the legacy
`{"rgb": [r,g,b]}` format that Pote's resolver didn't recognise.
Now uses `Alaja.Theme.install_template/1` with the correct flat
`[r,g,b]` format and the full 22-key colour set.
- **BUG**: every escript started with `:theme_active` unset in
Application env, even when the user had persisted a theme choice.
`Alaja.Application.start/2` now calls `Alaja.Config.ensure_loaded/0`
BEFORE `Theme.register_with_pote/0`. `Config.ensure_loaded/0` is now
public.
- **BUG**: `dispatch_main/1` never started the OTP application, so
`Theme.register_with_pote/0` was never invoked in escript mode.
- **BUG**: `subcommand` DSL macro produced flat `@commands` entries
with `subcommands: %{}` — inner `command` macros accumulated at the
top level instead of nesting under the parent.
- **BUG**: 14 `System.halt(1)` calls in library-accessible paths
killed the entire BEAM when used as a library; replaced with
`exit({:shutdown, 1})`.
- **BUG**: `Json.render/2` produced non-deterministic key order for
maps; keys are now sorted recursively via `Jason.OrderedObject`.
- **BUG**: division by zero in `Pulsar.render_frame/3` when
`pulse_chars: []` was passed.
- **BUG**: `batamanta` dep version bumped from 1.5.1 to 1.6.0.
- **LINT**: all Credo `--strict` issues resolved (cyclomatic complexity,
alias ordering, `cond` → `if`, implicit `try`/`catch`).
- **LINT**: all compiler warnings eliminated (unused variables,
ungrouped function clauses, never-match patterns, dead code).
- **LINT**: `__before_compile__/1` macro no longer generates dead
`if` block when `@halt_on_error` is `false`.
- **LINT**: `dispatch/2` has a proper `@spec` so external consumers
get a known return type instead of `dynamic()`.
### Migration
- If you called `ColorWheel.render/2` (returning iodata), migrate to
`ColorWheel.render_for_terminal/2` and pattern-match on the tagged
tuple, or use the new `ColorWheel.render/2` which returns
`Buffer.t/0`.
- If you called `Table.render/2` expecting iodata, switch to
`Table.render_iodata/2` (or wrap with `Buffer.to_iodata/1`).
- If you called `Box.render/2` and composed the result with iodata,
switch to `Buffer.to_iodata(Box.render(...))` or use `Box.print/2`.
- If you wrote `cast_flag_value/3` test fixtures, the new `:path, :url,
:color_list` types now exist; the failure mode for bad
`:integer/:float` input has changed from "raise" to "fall back to
default".
- Downstream consumers (Delfos, Apero, etc.) that only use
`X.print(...)` and `Alaja.print_raw(string)` see **zero breaking
changes**.
## [1.0.0] - 2026-06-10
### Added
- Initial open source release: DSL, components, rendering, syntax
highlighting, ANSI utilities.
[2.2.0]: https://hex.pm/packages/alaja/2.2.0
[2.1.0]: https://hex.pm/packages/alaja/2.1.0
[2.0.0]: https://hex.pm/packages/alaja/2.0.0
[1.0.0]: https://hex.pm/packages/alaja/1.0.0
> ## A note on history
>
> The git history of this repository was reset as part of a deliberate
> cleanup effort. The commits you can read here describe the codebase as
> it stands today — they do not preserve the original chronology of
> development.
>
> Anything worth keeping from before the reset was carried forward as
> tagged releases with explicit `CHANGELOG.md` entries. Anything not
> preserved is, by the maintainer's choice, no longer part of the
> canonical development line.
>
> Tag `1.0.0` points to the initial open-source cut-over; tag `2.0.0`
> points to the current HEAD and the canonical Buffer-first release.
> All versioned artifacts on Hex.pm and GitHub Releases follow this
> convention.