Skip to content

Commit 3846164

Browse files
ST0008: orthogonalising output styling and formatting
1 parent 750176f commit 3846164

File tree

8 files changed

+1089
-36
lines changed

8 files changed

+1089
-36
lines changed

intent/st/ST0008/done.md

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

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

55
## Completed Work Packages
66

7-
### WP1: Context Module Foundation
7+
### WP1: Context Module Foundation
88

99
**Completed**: 2025-09-22
1010
**Size**: S
@@ -31,7 +31,7 @@
3131

3232
---
3333

34-
### WP2: Plain Renderer Implementation
34+
### WP2: Plain Renderer Implementation
3535

3636
**Completed**: 2025-09-22
3737
**Size**: S
@@ -40,7 +40,7 @@
4040

4141
- Created `lib/arca_cli/output/plain_renderer.ex`
4242
- Implemented message renderers for all semantic types:
43-
- Success (), Error (), Warning (), Info, Text
43+
- Success (), Error (), Warning (), Info, Text
4444
- Implemented table renderer using Owl with `:solid` border style
4545
- Automatic column width calculation
4646
- Header support with proper formatting
@@ -68,15 +68,100 @@
6868

6969
---
7070

71+
### WP7: Global CLI Options
72+
73+
**Completed**: 2025-09-22
74+
**Size**: S
75+
76+
**Delivered**:
77+
78+
- Added global `--cli-style` option with values: fancy, plain, dump
79+
- Added global `--cli-no-ansi` flag as alias for `--cli-style plain`
80+
- Implemented environment variable support:
81+
- `NO_COLOR` sets style to plain
82+
- `ARCA_STYLE` sets style to specified value
83+
- Established precedence order: CLI flags > CLI options > env vars > settings
84+
- Integrated style settings into command pipeline via `merge_style_settings/2`
85+
- Style automatically flows through to Context metadata
86+
- Prevented duplicate global options when multiple configurators are used
87+
88+
**Files Modified**:
89+
90+
- `lib/arca_cli/configurator/base_configurator.ex` - Added global options configuration
91+
- `lib/arca_cli/configurator/coordinator.ex` - Added single-point global option injection
92+
- `lib/arca_cli.ex` - Added merge_style_settings to pass style through pipeline
93+
94+
**Files Created**:
95+
96+
- `test/arca_cli/global_options_test.exs` - Comprehensive test suite (17 tests)
97+
98+
**Key Implementation Notes**:
99+
100+
- Used `--cli-` prefix to avoid conflicts with command-specific options
101+
- Global options added only once at Coordinator level to prevent duplicates
102+
- Environment variable handling includes proper precedence
103+
- Full test coverage including isolation of environment variables
104+
105+
---
106+
107+
### WP3: Fancy Renderer Implementation ✅
108+
109+
**Completed**: 2025-09-22
110+
**Size**: M
111+
112+
**Delivered**:
113+
114+
- Created `lib/arca_cli/output/fancy_renderer.ex` with full color and symbol support
115+
- Implemented colored message renderers for all semantic types:
116+
- Success (✓) with green color
117+
- Error (✗) with red color
118+
- Warning (⚠) with yellow color
119+
- Info (ℹ) with cyan color
120+
- Implemented enhanced table renderer using Owl with `:solid_rounded` border style
121+
- Smart cell colorization based on content (headers, numbers, keywords)
122+
- Automatic conversion of list-of-lists format to maps for Owl compatibility
123+
- Implemented formatted lists with colored bullets
124+
- Configurable bullet colors
125+
- Optional titles with bright styling
126+
- Implemented spinner and progress display support
127+
- Executes functions and shows results with appropriate coloring
128+
- Added automatic TTY detection and fallback to PlainRenderer
129+
- Checks TERM environment variable
130+
- Respects explicit style setting in context metadata
131+
- Refactored to use pure functional Elixir patterns:
132+
- Pattern matching for render dispatch
133+
- Function chaining instead of if/then conditionals
134+
- Pipeline-based data transformations
135+
- Created comprehensive test suite with 24 tests
136+
- All tests passing (295 total in project)
137+
138+
**Files Created**:
139+
140+
- `lib/arca_cli/output/fancy_renderer.ex`
141+
- `test/arca_cli/output/fancy_renderer_test.exs`
142+
143+
**Key Implementation Notes**:
144+
145+
- Uses `:solid_rounded` border style for tables (not `:rounded` which doesn't exist in Owl)
146+
- Converts list-of-lists table format to maps for Owl compatibility
147+
- Smart cell colorization recognizes patterns (headers, numbers, success/error keywords)
148+
- TTY detection falls back gracefully to PlainRenderer when not in terminal
149+
- Follows pure functional patterns throughout with pattern matching and pipelines
150+
151+
---
152+
71153
## Test Coverage
72154

73-
- All tests passing (254 tests total in project)
155+
- All tests passing (295 tests total in project)
74156
- 100% coverage of implemented modules
75157
- Edge cases and error conditions fully tested
158+
- Environment variable isolation in tests
76159

77160
## Integration Status
78161

79162
- Context module ready for integration with command execution
80163
- PlainRenderer ready for use via Output pipeline (WP4)
164+
- FancyRenderer ready for use via Output pipeline (WP4)
165+
- Global CLI options functional and tested
81166
- No breaking changes to existing code
82-
- Full backwards compatibility maintained
167+
- Full backwards compatibility maintained

intent/st/ST0008/tasks.md

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,17 @@
22

33
## Progress Summary
44

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

7-
- ✅ Completed: WP1 (Context Module), WP2 (Plain Renderer) - See done.md
8-
- 🎯 Ready to Start: WP3 (Fancy Renderer), WP7 (Global Options)
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)
99
- ⏸️ Blocked: WP4-WP6, WP8-WP10 (waiting on dependencies)
1010

1111
## Remaining Work Packages
1212

13-
### WP3: Fancy Renderer Implementation
14-
15-
**Status**: Ready to Start
16-
**Size**: M
17-
**Description**: Implement the fancy renderer with colors, symbols, and Owl formatting.
18-
19-
**Tasks**:
20-
21-
- [ ] Create `lib/arca_cli/output/fancy_renderer.ex`
22-
- [ ] Implement colored message renderers (success, error, warning, info)
23-
- [ ] Implement Owl.Table integration for `{:table, rows, opts}`
24-
- [ ] Implement formatted lists with colored bullets
25-
- [ ] Implement spinner support for `{:spinner, label, func}`
26-
- [ ] Handle non-TTY fallback to plain renderer
27-
- [ ] Add tests with Owl output verification
28-
- [ ] Document color scheme and symbols used
29-
30-
---
31-
3213
### WP4: Output Module Pipeline
3314

34-
**Status**: Blocked (requires WP3)
15+
**Status**: Ready to Start
3516
**Size**: M
3617
**Description**: Create the main `Arca.Cli.Output` module that orchestrates the rendering pipeline.
3718

@@ -160,18 +141,18 @@
160141
## Dependencies Graph
161142

162143
```
163-
WP1 ✅ ──┬──> WP3 🎯 ──> WP4 ──┬──> WP5
144+
WP1 ✅ ──┬──> WP3 ──> WP4 🎯 ──┬──> WP5
164145
│ ├──> WP6 ──┬──> WP8
165146
WP2 ✅ ──┘ │ └──> WP9
166147
└──> WP10
167-
WP7 🎯 (independent)
148+
WP7 ✅ (completed)
168149
```
169150

170151
## Next Steps
171152

172-
1. **Start WP3 (Fancy Renderer)** - Build on PlainRenderer pattern with colors and formatting
173-
2. **Start WP7 (Global Options)** - Can be done in parallel, no dependencies
174-
3. **Then WP4 (Output Pipeline)** - Once WP3 is complete, create the orchestration layer
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
175156

176157
## Success Metrics
177158

lib/arca_cli.ex

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,9 @@ defmodule Arca.Cli do
560560
def handle_args(args, settings, optimus) do
561561
case args do
562562
{:ok, [subcmd], args} ->
563-
handle_subcommand(subcmd, args, settings, optimus)
563+
# Extract global CLI options and merge into settings
564+
settings_with_style = merge_style_settings(args, settings)
565+
handle_subcommand(subcmd, args, settings_with_style, optimus)
564566

565567
{:ok, msg} when is_binary(msg) ->
566568
msg
@@ -738,6 +740,64 @@ defmodule Arca.Cli do
738740
end
739741
end
740742

743+
@doc """
744+
Merges style-related settings from CLI options and environment variables.
745+
746+
Precedence order:
747+
1. CLI flags (--cli-style, --cli-no-ansi)
748+
2. Environment variables (NO_COLOR, ARCA_STYLE)
749+
3. Existing settings
750+
751+
## Parameters
752+
- args: Parsed command arguments containing options and flags
753+
- settings: Existing application settings
754+
755+
## Returns
756+
- Updated settings map with style preference
757+
"""
758+
@spec merge_style_settings(map(), map()) :: map()
759+
def merge_style_settings(args, settings) do
760+
# Start with existing settings
761+
settings
762+
|> apply_env_style()
763+
|> apply_cli_style(args)
764+
end
765+
766+
# Apply style from environment variables
767+
defp apply_env_style(settings) do
768+
cond do
769+
# NO_COLOR takes precedence over ARCA_STYLE
770+
System.get_env("NO_COLOR") not in [nil, "", "0", "false"] ->
771+
Map.put(settings, "style", "plain")
772+
773+
arca_style = System.get_env("ARCA_STYLE") ->
774+
if arca_style in ["fancy", "plain", "dump"] do
775+
Map.put(settings, "style", arca_style)
776+
else
777+
settings
778+
end
779+
780+
true ->
781+
settings
782+
end
783+
end
784+
785+
# Apply style from CLI options (highest precedence)
786+
defp apply_cli_style(settings, args) do
787+
cond do
788+
# Check if --cli-no-ansi flag is set
789+
Map.get(args, :flags, %{}) |> Map.get(:cli_no_ansi, false) ->
790+
Map.put(settings, "style", "plain")
791+
792+
# Check if --cli-style option is set
793+
cli_style = Map.get(args, :options, %{}) |> Map.get(:cli_style) ->
794+
Map.put(settings, "style", Atom.to_string(cli_style))
795+
796+
true ->
797+
settings
798+
end
799+
end
800+
741801
@doc """
742802
Determines if help should be shown for a command with the given arguments.
743803

lib/arca_cli/configurator/base_configurator.ex

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ defmodule Arca.Cli.Configurator.BaseConfigurator do
140140
@impl Arca.Cli.Configurator.ConfiguratorBehaviour
141141
def setup do
142142
create_base_config()
143+
|> add_global_options()
143144
|> inject_subcommands()
144145
|> Optimus.new!()
145146
end
@@ -192,6 +193,33 @@ defmodule Arca.Cli.Configurator.BaseConfigurator do
192193
]
193194
end
194195

196+
# Add global CLI options only once (will be called by setup/0 for single configurator)
197+
def add_global_options(config) do
198+
Keyword.merge(config,
199+
options: [
200+
cli_style: [
201+
value_name: "STYLE",
202+
long: "--cli-style",
203+
help: "Set output style (fancy, plain, dump)",
204+
required: false,
205+
parser: fn s ->
206+
case String.downcase(s) do
207+
style when style in ["fancy", "plain", "dump"] -> {:ok, String.to_atom(style)}
208+
_ -> {:error, "Invalid style. Must be one of: fancy, plain, dump"}
209+
end
210+
end
211+
]
212+
],
213+
flags: [
214+
cli_no_ansi: [
215+
long: "--cli-no-ansi",
216+
help: "Disable ANSI colors in output (same as --cli-style plain)",
217+
multiple: false
218+
]
219+
]
220+
)
221+
end
222+
195223
def inject_subcommands(optimus, commands \\ commands()) do
196224
# Process commands
197225
processed_commands =

lib/arca_cli/configurator/coordinator.ex

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,11 @@ defmodule Arca.Cli.Configurator.Coordinator do
6767
{:ok, {combined_config, subcommand_names}} <- combine_configurator_data(unique_cfg8rs),
6868
:ok <- check_for_duplicated_commands(subcommand_names),
6969
{:ok, final_config} <- create_final_config(combined_config, unique_cfg8rs) do
70+
# Add global options once at the coordinator level
71+
final_config_with_options = add_global_cli_options(final_config)
72+
7073
# Create the Optimus configuration
71-
Optimus.new!(final_config)
74+
Optimus.new!(final_config_with_options)
7275
else
7376
# Fallback to ensure we always return an Optimus configuration even if errors occurred
7477
{:error, _error_type, reason} ->
@@ -382,6 +385,47 @@ defmodule Arca.Cli.Configurator.Coordinator do
382385
end
383386
end
384387

388+
@doc """
389+
Add global CLI options to the configuration.
390+
391+
These options are added at the coordinator level to ensure they're only added once,
392+
even when multiple configurators are used.
393+
394+
## Parameters
395+
- config: Configuration keyword list
396+
397+
## Returns
398+
- Configuration with global options added
399+
"""
400+
@spec add_global_cli_options(keyword()) :: keyword()
401+
def add_global_cli_options(config) do
402+
global_options = [
403+
options: [
404+
cli_style: [
405+
value_name: "STYLE",
406+
long: "--cli-style",
407+
help: "Set output style (fancy, plain, dump)",
408+
required: false,
409+
parser: fn s ->
410+
case String.downcase(s) do
411+
style when style in ["fancy", "plain", "dump"] -> {:ok, String.to_atom(style)}
412+
_ -> {:error, "Invalid style. Must be one of: fancy, plain, dump"}
413+
end
414+
end
415+
]
416+
],
417+
flags: [
418+
cli_no_ansi: [
419+
long: "--cli-no-ansi",
420+
help: "Disable ANSI colors in output (same as --cli-style plain)",
421+
multiple: false
422+
]
423+
]
424+
]
425+
426+
Keyword.merge(config, global_options)
427+
end
428+
385429
@doc """
386430
Merges subcommands, with newer commands taking precedence.
387431

0 commit comments

Comments
 (0)