Skip to content

Commit 9225edb

Browse files
ST0008: orthogonalising output styling and formatting
1 parent 3846164 commit 9225edb

File tree

6 files changed

+618
-59
lines changed

6 files changed

+618
-59
lines changed

intent/st/ST0008/done.md

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

3-
## Progress: 40% Complete (4 of 10 work packages)
3+
## Progress: 50% Complete (5 of 10 work packages)
44

55
## Completed Work Packages
66

@@ -150,9 +150,69 @@
150150

151151
---
152152

153+
### WP4: Output Module Pipeline ✅
154+
155+
**Completed**: 2025-09-22
156+
**Size**: M
157+
158+
**Delivered**:
159+
160+
- Created `lib/arca_cli/output.ex` as main orchestration module
161+
- Implemented complete rendering pipeline:
162+
- `render/1` main entry point
163+
- Smart style determination with precedence chain
164+
- Renderer dispatch to fancy, plain, or dump
165+
- Final output formatting
166+
- Implemented style precedence (highest to lowest):
167+
- Explicit style in context metadata
168+
- NO_COLOR environment variable
169+
- ARCA_STYLE environment variable
170+
- MIX_ENV=test detection
171+
- TTY availability check
172+
- Added dump renderer for debugging:
173+
- Shows complete Context structure
174+
- Uses `inspect/2` with pretty printing
175+
- Useful for development and troubleshooting
176+
- Implemented environment detection helpers:
177+
- `no_color?/0` - Checks NO_COLOR with proper value handling
178+
- `env_style/0` - Gets style from ARCA_STYLE
179+
- `test_env?/0` - Detects test environment
180+
- `tty?/0` - Checks for TTY availability
181+
- Added `current_style/1` helper for testing and debugging
182+
- Enhanced error handling:
183+
- Handles nil contexts gracefully
184+
- Handles malformed output (non-list values)
185+
- Always returns a string, never nil
186+
- Updated both renderers to handle edge cases:
187+
- FancyRenderer handles nil/invalid output
188+
- PlainRenderer handles nil/invalid output
189+
- Fixed atom key handling in tables
190+
- Created comprehensive test suite with 30 tests
191+
- All tests passing (325 total in project)
192+
193+
**Files Created**:
194+
195+
- `lib/arca_cli/output.ex`
196+
- `test/arca_cli/output_test.exs`
197+
198+
**Files Modified**:
199+
200+
- `lib/arca_cli/output/fancy_renderer.ex` - Added nil/invalid output handling
201+
- `lib/arca_cli/output/plain_renderer.ex` - Fixed atom key handling and nil output
202+
203+
**Key Implementation Notes**:
204+
205+
- Style determination uses pure functional pattern matching
206+
- Dump format useful for debugging command output
207+
- NO_COLOR properly handles "0", "false", and empty string as false
208+
- Test environment automatically uses plain style
209+
- Renderers gracefully handle malformed data
210+
211+
---
212+
153213
## Test Coverage
154214

155-
- All tests passing (295 tests total in project)
215+
- All tests passing (325 tests total in project)
156216
- 100% coverage of implemented modules
157217
- Edge cases and error conditions fully tested
158218
- Environment variable isolation in tests
@@ -161,7 +221,8 @@
161221

162222
- Context module ready for integration with command execution
163223
- PlainRenderer ready for use via Output pipeline (WP4)
164-
- FancyRenderer ready for use via Output pipeline (WP4)
224+
- FancyRenderer integrated with Output pipeline
225+
- Output module fully functional and tested
165226
- Global CLI options functional and tested
166227
- No breaking changes to existing code
167-
- Full backwards compatibility maintained
228+
- Full backwards compatibility maintained

intent/st/ST0008/tasks.md

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,17 @@
22

33
## Progress Summary
44

5-
**Overall Status**: 40% Complete (4 of 10 work packages)
5+
**Overall Status**: 50% Complete (5 of 10 work packages)
66

7-
- ✅ Completed: WP1 (Context Module), WP2 (Plain Renderer), WP3 (Fancy Renderer), WP7 (Global Options) - See done.md
8-
- 🎯 Ready to Start: WP4 (Output Module Pipeline)
7+
- ✅ Completed: WP1 (Context Module), WP2 (Plain Renderer), WP3 (Fancy Renderer), WP4 (Output Pipeline), WP7 (Global Options) - See done.md
8+
- 🎯 Ready to Start: WP5 (Callback Integration), WP6 (Command Execution Integration)
99
- ⏸️ Blocked: WP4-WP6, WP8-WP10 (waiting on dependencies)
1010

1111
## Remaining Work Packages
1212

13-
### WP4: Output Module Pipeline
14-
15-
**Status**: Ready to Start
16-
**Size**: M
17-
**Description**: Create the main `Arca.Cli.Output` module that orchestrates the rendering pipeline.
18-
19-
**Tasks**:
20-
21-
- [ ] Create `lib/arca_cli/output.ex`
22-
- [ ] Implement `render/1` main entry point
23-
- [ ] Implement `determine_style/1` for auto-detection logic
24-
- [ ] Implement `apply_renderer/1` to dispatch to correct renderer
25-
- [ ] Implement `format_for_output/1` to prepare final string
26-
- [ ] Add support for NO_COLOR environment variable
27-
- [ ] Add support for MIX_ENV=test detection
28-
- [ ] Add TTY detection via `Owl.IO.terminal?/0`
29-
- [ ] Create comprehensive pipeline tests
30-
- [ ] Test style detection in various environments
31-
32-
---
33-
3413
### WP5: Callback Integration
3514

36-
**Status**: Blocked (requires WP4)
15+
**Status**: Ready to Start
3716
**Size**: S
3817
**Description**: Register new callback points and integrate with existing callback system.
3918

@@ -50,7 +29,7 @@
5029

5130
### WP6: Command Execution Integration
5231

53-
**Status**: Blocked (requires WP4)
32+
**Status**: Ready to Start (can be done in parallel with WP5)
5433
**Size**: M
5534
**Description**: Update `Arca.Cli.execute_command/5` to handle Ctx returns while maintaining backwards compatibility.
5635

@@ -141,18 +120,18 @@
141120
## Dependencies Graph
142121

143122
```
144-
WP1 ✅ ──┬──> WP3 ✅ ──> WP4 🎯 ──┬──> WP5
145-
│ ├──> WP6 ──┬──> WP8
123+
WP1 ✅ ──┬──> WP3 ✅ ──> WP4 ──┬──> WP5 🎯
124+
│ ├──> WP6 🎯 ──┬──> WP8
146125
WP2 ✅ ──┘ │ └──> WP9
147126
└──> WP10
148127
WP7 ✅ (completed)
149128
```
150129

151130
## Next Steps
152131

153-
1. **Start WP4 (Output Module Pipeline)** - Create the main orchestration layer that uses the renderers
154-
2. **Then WP5 (Callback Integration)** - Register the new callback points
155-
3. **Then WP6 (Command Execution Integration)** - Update execute_command to handle Ctx returns
132+
1. **Start WP5 (Callback Integration)** - Register the new `:format_command_result` callback
133+
2. **Start WP6 (Command Execution Integration)** - Update execute_command to handle Ctx returns (can be done in parallel)
134+
3. **Then WP8 (Sample Command Migration)** - Migrate AboutCommand as proof of concept
156135

157136
## Success Metrics
158137

lib/arca_cli/output.ex

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
defmodule Arca.Cli.Output do
2+
@moduledoc """
3+
Main output orchestration module for Arca.CLI.
4+
5+
This module provides the central rendering pipeline that:
6+
- Determines the appropriate output style based on context and environment
7+
- Dispatches to the correct renderer (fancy, plain, or dump)
8+
- Handles the complete transformation from Context to final output string
9+
10+
## Style Precedence
11+
12+
The output style is determined by the following precedence (highest to lowest):
13+
1. Explicit style in context metadata
14+
2. NO_COLOR environment variable (forces plain)
15+
3. ARCA_STYLE environment variable
16+
4. MIX_ENV=test (forces plain in test environment)
17+
5. TTY availability (fancy if TTY, plain otherwise)
18+
19+
## Available Styles
20+
21+
- `:fancy` - Full colors, symbols, and enhanced formatting (TTY only)
22+
- `:plain` - No ANSI codes, plain text with Unicode symbols
23+
- `:dump` - Raw data dump for debugging, shows Context structure
24+
25+
## Examples
26+
27+
iex> ctx = %Arca.Cli.Ctx{output: [{:success, "Done"}]}
28+
iex> Output.render(ctx)
29+
"✓ Done"
30+
31+
iex> ctx = %Arca.Cli.Ctx{output: [{:error, "Failed"}], meta: %{style: :dump}}
32+
iex> Output.render(ctx)
33+
"%Arca.Cli.Ctx{...}"
34+
"""
35+
36+
alias Arca.Cli.Ctx
37+
alias Arca.Cli.Output.{FancyRenderer, PlainRenderer}
38+
require Logger
39+
40+
@doc """
41+
Renders a Context to final output string.
42+
43+
Takes a Context struct and renders it according to the determined style,
44+
returning a string ready for output.
45+
46+
## Parameters
47+
- ctx: The Context struct to render
48+
49+
## Returns
50+
- Formatted string output
51+
"""
52+
@spec render(Ctx.t() | nil) :: String.t()
53+
def render(nil), do: ""
54+
55+
def render(%Ctx{} = ctx) do
56+
ctx
57+
|> determine_style()
58+
|> apply_renderer(ctx)
59+
|> format_for_output()
60+
end
61+
62+
# Style determination with precedence chain
63+
defp determine_style(%Ctx{meta: %{style: style}}) when style in [:fancy, :plain, :dump] do
64+
style
65+
end
66+
67+
defp determine_style(%Ctx{} = ctx) do
68+
case check_environment() do
69+
{:style, style} -> style
70+
:auto -> auto_detect_style(ctx)
71+
end
72+
end
73+
74+
# Check environment variables and settings
75+
defp check_environment do
76+
cond do
77+
no_color?() -> {:style, :plain}
78+
style = env_style() -> {:style, style}
79+
test_env?() -> {:style, :plain}
80+
true -> :auto
81+
end
82+
end
83+
84+
# Auto-detect based on TTY availability
85+
defp auto_detect_style(_ctx) do
86+
case tty?() do
87+
true -> :fancy
88+
false -> :plain
89+
end
90+
end
91+
92+
# Renderer dispatch
93+
defp apply_renderer(:fancy, ctx), do: FancyRenderer.render(ctx)
94+
defp apply_renderer(:plain, ctx), do: PlainRenderer.render(ctx) |> IO.iodata_to_binary()
95+
defp apply_renderer(:dump, ctx), do: dump_context(ctx)
96+
97+
# Dump renderer for debugging
98+
defp dump_context(%Ctx{} = ctx) do
99+
%{
100+
command: ctx.command,
101+
args: ctx.args,
102+
options: ctx.options,
103+
output: ctx.output,
104+
errors: ctx.errors,
105+
status: ctx.status,
106+
cargo: ctx.cargo,
107+
meta: ctx.meta
108+
}
109+
|> inspect(pretty: true, width: 80, limit: :infinity)
110+
end
111+
112+
# Final formatting - ensure we always return a string
113+
defp format_for_output(result) when is_binary(result), do: result
114+
defp format_for_output(result) when is_list(result), do: IO.iodata_to_binary(result)
115+
defp format_for_output(nil), do: ""
116+
defp format_for_output(result), do: to_string(result)
117+
118+
# Environment detection helpers
119+
120+
defp no_color? do
121+
case System.get_env("NO_COLOR") do
122+
nil -> false
123+
"" -> false
124+
"0" -> false
125+
"false" -> false
126+
_ -> true
127+
end
128+
end
129+
130+
defp env_style do
131+
case System.get_env("ARCA_STYLE") do
132+
"fancy" -> :fancy
133+
"plain" -> :plain
134+
"dump" -> :dump
135+
_ -> nil
136+
end
137+
end
138+
139+
defp test_env? do
140+
case System.get_env("MIX_ENV") do
141+
"test" -> true
142+
_ -> false
143+
end
144+
end
145+
146+
defp tty? do
147+
# Check multiple indicators for TTY availability
148+
case {System.get_env("TERM"), IO.ANSI.enabled?()} do
149+
{nil, _} -> false
150+
{"dumb", _} -> false
151+
{_, false} -> false
152+
{_, true} -> true
153+
end
154+
end
155+
156+
@doc """
157+
Returns the current output style that would be used for rendering.
158+
159+
Useful for debugging and testing style determination logic.
160+
161+
## Parameters
162+
- ctx: Optional context to check for style metadata
163+
164+
## Returns
165+
- The style atom (:fancy, :plain, or :dump)
166+
167+
## Examples
168+
169+
iex> Output.current_style()
170+
:fancy
171+
172+
iex> ctx = %Ctx{meta: %{style: :plain}}
173+
iex> Output.current_style(ctx)
174+
:plain
175+
"""
176+
@spec current_style(Ctx.t() | nil) :: :fancy | :plain | :dump
177+
def current_style(ctx \\ nil) do
178+
case ctx do
179+
nil -> determine_style(%Ctx{})
180+
%Ctx{} = context -> determine_style(context)
181+
_ -> determine_style(%Ctx{})
182+
end
183+
end
184+
end

0 commit comments

Comments
 (0)