Skip to content

Commit bba1260

Browse files
ST0008: orthogonalising output styling and formatting
1 parent f7eccf6 commit bba1260

File tree

4 files changed

+338
-87
lines changed

4 files changed

+338
-87
lines changed

intent/st/ST0008/done.md

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Completed Work - ST0008: Orthogonalised formatting and outputting
22

3-
## Progress: 70% Complete (7 of 10 work packages)
3+
## Progress: 80% Complete (8 of 10 work packages)
44

55
## Completed Work Packages
66

@@ -104,14 +104,14 @@
104104

105105
---
106106

107-
### WP3: Fancy Renderer Implementation ✅
107+
### WP3: ANSI Renderer Implementation (formerly Fancy Renderer)
108108

109109
**Completed**: 2025-09-22
110110
**Size**: M
111111

112112
**Delivered**:
113113

114-
- Created `lib/arca_cli/output/fancy_renderer.ex` with full color and symbol support
114+
- Created `lib/arca_cli/output/ansi_renderer.ex` (renamed from fancy_renderer.ex) with full color and symbol support
115115
- Implemented colored message renderers for all semantic types:
116116
- Success (✓) with green color
117117
- Error (✗) with red color
@@ -293,7 +293,7 @@
293293

294294
- `lib/arca_cli.ex` - Added pattern-matched result processing
295295
- `lib/arca_cli/commands/sys_info_command.ex` - Refactored to use Context
296-
- `lib/arca_cli/output/fancy_renderer.ex` - Fixed ANSI code handling with Owl
296+
- `lib/arca_cli/output/ansi_renderer.ex` - Fixed ANSI code handling with Owl (renamed from fancy_renderer.ex)
297297
- `lib/arca_cli/output/plain_renderer.ex` - Fixed header detection logic
298298
- `lib/arca_cli/commands/about_command.ex` - Fixed return value
299299
- `lib/mix/tasks/arca_cli.ex` - Attempted fix for :ok printing (not needed)
@@ -311,20 +311,105 @@
311311

312312
---
313313

314+
### Style Renaming and JSON Renderer
315+
316+
**Completed**: 2025-09-22
317+
**Size**: S
318+
319+
**Delivered**:
320+
321+
- Renamed output styles for clarity and consistency:
322+
- `fancy``ansi` (for ANSI color/symbol output)
323+
- `plain``plain` (unchanged)
324+
- `dump``dump` (unchanged)
325+
- Added new `json` style for structured JSON output
326+
- Created `lib/arca_cli/output/json_renderer.ex`:
327+
- Converts Context to JSON-serializable map
328+
- Pretty-prints JSON output using Jason
329+
- Handles all output types (success, error, warning, info, text, table, list)
330+
- Filters out empty fields for clean output
331+
- Updated all references throughout codebase:
332+
- Renamed FancyRenderer module to AnsiRenderer
333+
- Updated all test files and references
334+
- Updated environment variable handling
335+
- Updated global CLI options
336+
- Maintained full backwards compatibility
337+
- All 306 tests passing after refactor
338+
339+
**Files Created**:
340+
341+
- `lib/arca_cli/output/json_renderer.ex`
342+
- `test/arca_cli/output/json_renderer_test.exs`
343+
344+
**Files Renamed**:
345+
346+
- `lib/arca_cli/output/fancy_renderer.ex``lib/arca_cli/output/ansi_renderer.ex`
347+
- `test/arca_cli/output/fancy_renderer_test.exs``test/arca_cli/output/ansi_renderer_test.exs`
348+
349+
**Files Modified**:
350+
351+
- `lib/arca_cli/output.ex` - Updated style names and dispatch logic
352+
- `lib/arca_cli.ex` - Updated environment style checking
353+
- `test/arca_cli/output_test.exs` - Updated test expectations
354+
- Various test files - Updated style references
355+
356+
---
357+
358+
### WP8: Command Migration to Context Pattern ✅
359+
360+
**Completed**: 2025-09-22
361+
**Size**: M
362+
363+
**Delivered**:
364+
365+
- Migrated `settings.all` command to Context pattern:
366+
- Converted from `inspect` output to structured table
367+
- Added table with columns: "Setting", "Value", "Type"
368+
- Added type detection for values (string, integer, boolean, etc.)
369+
- Handles empty settings gracefully
370+
- Works with all four output styles (plain, ansi, json, dump)
371+
- Migrated `cli.history` command to Context pattern:
372+
- Converted from formatted string output to structured table
373+
- Added table with columns: "Index", "Command", "Arguments"
374+
- Parses command strings to separate command from arguments
375+
- Handles empty history gracefully
376+
- Added cargo data with total command count
377+
- Updated test expectations:
378+
- Fixed `test/arca_cli/cli/arca_cli_test.exs` to expect new table format
379+
- Tests now check for presence of table content rather than exact format
380+
- All 337 tests passing
381+
382+
**Files Modified**:
383+
384+
- `lib/arca_cli/commands/settings_all_command.ex` - Full Context migration with table output
385+
- `lib/arca_cli/commands/cli_history_command.ex` - Full Context migration with table output
386+
- `test/arca_cli/cli/arca_cli_test.exs` - Updated test expectations for new format
387+
388+
**Key Implementation Notes**:
389+
390+
- Both commands maintain backwards compatibility in behavior
391+
- Table formatting automatically adapts to output style
392+
- Type information in settings.all helps users understand configuration
393+
- Command/argument parsing in cli.history improves readability
394+
- Test updates ensure stability without being overly rigid about format
395+
396+
---
397+
314398
## Test Coverage
315399

316-
- All tests passing (306 tests total in project)
400+
- All tests passing (337 tests total in project)
317401
- 100% coverage of implemented modules
318402
- Edge cases and error conditions fully tested
319403
- Environment variable isolation in tests
320404

321405
## Integration Status
322406

323407
- Context module integrated with command execution pipeline
324-
- PlainRenderer and FancyRenderer fully functional
408+
- PlainRenderer, AnsiRenderer, and JsonRenderer fully functional
325409
- Output module integrated with Arca.Cli main flow
326410
- Callbacks integrated with rendering pipeline
327411
- Global CLI options functional and tested
328-
- sys.info command migrated to Context pattern
412+
- Three commands migrated to Context pattern: sys.info, settings.all, cli.history
329413
- No breaking changes to existing code
330414
- Full backwards compatibility maintained
415+
- Four output styles available: plain, ansi, json, dump

intent/st/ST0008/impl.md

Lines changed: 220 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,232 @@
11
# Implementation - ST0008: Orthogonalised formatting and outputting
22

3-
## Implementation
3+
## Implementation Overview
44

5-
[Notes on implementation details, decisions, challenges, and their resolutions]
5+
The orthogonalized output system separates data processing, styling, and formatting concerns through a layered architecture:
6+
7+
1. **Context Layer** (`Arca.Cli.Ctx`) - Carries structured command output
8+
2. **Renderer Layer** - Style-specific rendering (Plain, ANSI, JSON, Dump)
9+
3. **Output Layer** (`Arca.Cli.Output`) - Orchestrates rendering pipeline
10+
4. **Integration Layer** - Command execution and callback processing
11+
12+
## Architecture
13+
14+
```
15+
Command → Context → Callbacks → Output → Renderer → Final Output
16+
17+
Style Detection
18+
(ENV, CLI flags, TTY)
19+
```
620

721
## Code Examples
822

9-
[Key code snippets and examples]
23+
### Command Using Context Pattern
24+
25+
```elixir
26+
defmodule Arca.Cli.Commands.SettingsAllCommand do
27+
def handle(_args, settings, _optimus) do
28+
case Arca.Cli.load_settings() do
29+
{:ok, loaded_settings} ->
30+
table_rows = settings_to_table_rows(loaded_settings)
31+
32+
Ctx.new(:"settings.all", settings)
33+
|> Ctx.add_output({:info, "Current Configuration Settings"})
34+
|> Ctx.add_output({:table, table_rows, [has_headers: true]})
35+
|> Ctx.with_cargo(%{settings_count: map_size(loaded_settings)})
36+
|> Ctx.complete(:ok)
37+
38+
{:error, _reason} ->
39+
Ctx.new(:"settings.all", settings)
40+
|> Ctx.add_error("Failed to load settings")
41+
|> Ctx.complete(:error)
42+
end
43+
end
44+
end
45+
```
46+
47+
### Pattern-Matched Command Result Processing
48+
49+
```elixir
50+
# In lib/arca_cli.ex
51+
defp process_command_result(%Ctx{} = ctx, _handler, _settings) do
52+
{:ok, Output.render(ctx)}
53+
end
54+
55+
defp process_command_result(result, _handler, settings) when is_binary(result) do
56+
{:ok, apply_legacy_formatting(result, settings)}
57+
end
58+
59+
defp process_command_result({:error, _} = error, _handler, _settings) do
60+
error
61+
end
62+
63+
defp process_command_result({:nooutput, _} = result, _handler, _settings) do
64+
result
65+
end
66+
```
67+
68+
### Polymorphic Callback Support
69+
70+
```elixir
71+
defp apply_format_callback(value, callback) when is_binary(value) do
72+
callback.(value)
73+
end
74+
75+
defp apply_format_callback(%Ctx{} = ctx, callback) do
76+
callback.(ctx)
77+
end
78+
```
79+
80+
### ANSI Renderer with Owl Integration
81+
82+
```elixir
83+
defp apply_cell_color(:header, cell), do: Owl.Data.tag(cell, :bright)
84+
defp apply_cell_color(:number, cell), do: Owl.Data.tag(cell, :cyan)
85+
defp apply_cell_color(:success, cell), do: Owl.Data.tag(cell, :green)
86+
defp apply_cell_color(:error, cell), do: Owl.Data.tag(cell, :red)
87+
defp apply_cell_color(:boolean_true, cell), do: Owl.Data.tag(cell, :green)
88+
defp apply_cell_color(:boolean_false, cell), do: Owl.Data.tag(cell, :yellow)
89+
defp apply_cell_color(_, cell), do: cell
90+
```
1091

1192
## Technical Details
1293

13-
[Specific technical details and considerations]
94+
### Style Precedence Chain
95+
96+
1. Explicit style in Context metadata (highest priority)
97+
2. CLI flags (`--cli-style`, `--cli-no-ansi`)
98+
3. Environment variables (`NO_COLOR`, `ARCA_STYLE`)
99+
4. Test environment detection (`MIX_ENV=test`)
100+
5. TTY availability check (lowest priority)
101+
102+
### Output Types
103+
104+
- **Messages**: `:success`, `:error`, `:warning`, `:info`, `:text`
105+
- **Structured**: `:table`, `:list`
106+
- **Interactive**: `:spinner`, `:progress`
107+
108+
### Table Rendering
109+
110+
Tables support two formats:
111+
1. List of lists: `[["Name", "Age"], ["Alice", "30"]]`
112+
2. List of maps: `[%{name: "Alice", age: 30}]`
113+
114+
Headers are indicated via the `has_headers: true` option.
115+
116+
### Global CLI Options
117+
118+
```elixir
119+
options: [
120+
cli_style: [
121+
value_name: "STYLE",
122+
long: "--cli-style",
123+
help: "Set output style (ansi, plain, json, dump)",
124+
parser: fn s ->
125+
case String.downcase(s) do
126+
style when style in ["ansi", "plain", "json", "dump"] ->
127+
{:ok, String.to_atom(style)}
128+
_ ->
129+
{:error, "Invalid style. Must be one of: ansi, plain, json, dump"}
130+
end
131+
end
132+
]
133+
],
134+
flags: [
135+
cli_no_ansi: [
136+
long: "--cli-no-ansi",
137+
help: "Disable ANSI colors in output (same as --cli-style plain)"
138+
]
139+
]
140+
```
14141

15142
## Challenges & Solutions
16143

17-
[Challenges encountered during implementation and how they were resolved]
144+
### Challenge 1: ANSI Codes Breaking Table Column Widths
145+
146+
**Problem**: Raw ANSI escape sequences in cell content caused Owl to miscalculate column widths, resulting in misaligned tables.
147+
148+
**Solution**: Use `Owl.Data.tag/2` instead of raw `IO.ANSI` functions. Owl's tagging system preserves the actual string length for width calculations while applying colors during rendering.
149+
150+
### Challenge 2: Header Detection in Tables
151+
152+
**Initial Approach**: Tried heuristic detection (checking if first row contains only strings).
153+
154+
**Problem**: Unreliable and could misidentify data rows as headers.
155+
156+
**Solution**: Explicit `has_headers: true` option in table output tuple. Commands explicitly indicate when first row contains headers.
157+
158+
### Challenge 3: Test Pollution from Callbacks
159+
160+
**Problem**: Callbacks registered in tests were leaking between test runs, causing intermittent failures with "[LEGACY]" prefixes appearing randomly.
161+
162+
**Solution**:
163+
- Made polymorphic formatter test synchronous (`async: false`)
164+
- Added proper cleanup with `on_exit` callbacks
165+
- Save and restore Application environment in test setup/teardown
166+
167+
### Challenge 4: Mix.env() Not Available in Production
168+
169+
**Problem**: Initial implementation used `Mix.env()` for test detection, which isn't available in production builds.
170+
171+
**Solution**: Use `System.get_env("MIX_ENV")` instead, which works in all environments.
172+
173+
### Challenge 5: Backwards Compatibility
174+
175+
**Problem**: Need to support both legacy string returns and new Context returns without breaking existing commands.
176+
177+
**Solution**: Pattern-matched dispatch in `process_command_result/3` that detects return type and applies appropriate processing path.
178+
179+
### Challenge 6: Style Naming Confusion
180+
181+
**Problem**: Original names ("fancy", "plain", "dump") weren't intuitive or standard.
182+
183+
**Solution**: Renamed to industry-standard terms:
184+
- `fancy``ansi` (clear indication of ANSI color support)
185+
- `plain``plain` (unchanged)
186+
- Added `json` for structured output
187+
- Kept `dump` for debugging
188+
189+
## Migration Path
190+
191+
Commands can be migrated incrementally:
192+
193+
1. **Phase 1**: Existing commands continue returning strings (fully supported)
194+
2. **Phase 2**: New commands use Context pattern
195+
3. **Phase 3**: Gradually migrate existing commands as needed
196+
197+
Example migration:
198+
199+
```elixir
200+
# Before
201+
def handle(args, settings, optimus) do
202+
result = process_data()
203+
formatted = format_as_string(result)
204+
IO.puts(formatted)
205+
formatted
206+
end
207+
208+
# After
209+
def handle(args, settings, optimus) do
210+
result = process_data()
211+
212+
Ctx.new(:my_command, settings)
213+
|> Ctx.add_output({:info, "Processing complete"})
214+
|> Ctx.add_output({:table, result, [has_headers: true]})
215+
|> Ctx.complete(:ok)
216+
end
217+
```
218+
219+
## Performance Considerations
220+
221+
- Context creation is lightweight (simple struct initialization)
222+
- Rendering is lazy - only happens when Output.render is called
223+
- Callback processing uses pattern matching for efficiency
224+
- No performance regression observed in testing (337 tests run in ~2.3s)
225+
226+
## Testing Strategy
227+
228+
- Unit tests for each renderer verify output format
229+
- Integration tests verify end-to-end command execution
230+
- Environment variable isolation prevents test pollution
231+
- Polymorphic callbacks tested with both string and Context inputs
232+
- Edge cases (nil output, malformed data) handled gracefully

0 commit comments

Comments
 (0)