|
| 1 | +# Design - ST0008: Orthogonalised formatting and outputting |
| 2 | + |
| 3 | +## Approach |
| 4 | + |
| 5 | +Leverage Arca.Cli's existing callback system (ST0003) to create an orthogonal output system that: |
| 6 | + |
| 7 | +1. Extends callbacks beyond REPL mode to ALL command execution |
| 8 | +2. Adds a context structure for carrying structured output data |
| 9 | +3. Provides style renderers for different output modes (fancy, plain, dump) |
| 10 | +4. Maintains full backwards compatibility with existing commands |
| 11 | + |
| 12 | +The system follows pure functional Elixir idioms with composable functions and pattern matching throughout. |
| 13 | + |
| 14 | +## Design Decisions |
| 15 | + |
| 16 | +### 1. Build on Existing Callback Infrastructure |
| 17 | + |
| 18 | +- **Decision**: Extend the existing `Arca.Cli.Callbacks` module rather than creating new infrastructure |
| 19 | +- **Rationale**: Minimizes complexity, leverages proven code, maintains consistency |
| 20 | + |
| 21 | +### 2. Context-Based Data Structure |
| 22 | + |
| 23 | +- **Decision**: Use a `%Arca.Cli.Ctx{}` struct to carry command output |
| 24 | +- **Rationale**: |
| 25 | + - Provides clean separation between data and presentation |
| 26 | + - Enables composable pipeline processing |
| 27 | + - Follows established patterns from Multiplyer/MeetZaya |
| 28 | + |
| 29 | +### 3. Semantic Output Types |
| 30 | + |
| 31 | +- **Decision**: Use tagged tuples for output items (e.g., `{:success, msg}`, `{:table, rows, opts}`) |
| 32 | +- **Rationale**: |
| 33 | + - Enables pattern matching for different renderers |
| 34 | + - Self-documenting output intent |
| 35 | + - Extensible for new output types |
| 36 | + |
| 37 | +### 4. Automatic Style Detection |
| 38 | + |
| 39 | +- **Decision**: Auto-detect appropriate style based on environment |
| 40 | +- **Rationale**: |
| 41 | + - Plain style in tests (MIX_ENV=test) |
| 42 | + - Plain style for non-TTY environments |
| 43 | + - Respects NO_COLOR=1 environment variable |
| 44 | + - Fancy style for interactive terminals |
| 45 | + |
| 46 | +### 5. Full Backwards Compatibility |
| 47 | + |
| 48 | +- **Decision**: Support all existing command return patterns |
| 49 | +- **Rationale**: |
| 50 | + - No breaking changes for existing commands |
| 51 | + - Gradual migration path |
| 52 | + - Opt-in adoption for new features |
| 53 | + |
| 54 | +## Architecture |
| 55 | + |
| 56 | +### Core Components |
| 57 | + |
| 58 | +``` |
| 59 | +┌─────────────────────┐ |
| 60 | +│ Command Handler │ |
| 61 | +│ returns: Ctx|Value │ |
| 62 | +└──────────┬──────────┘ |
| 63 | + │ |
| 64 | + ▼ |
| 65 | +┌─────────────────────┐ |
| 66 | +│ execute_command │ |
| 67 | +│ checks return type │ |
| 68 | +└──────────┬──────────┘ |
| 69 | + │ |
| 70 | + ▼ |
| 71 | +┌────────────────────────┐ |
| 72 | +│ Callbacks System │ |
| 73 | +│ :format_command_result │ |
| 74 | +└──────────┬─────────────┘ |
| 75 | + │ |
| 76 | + ▼ |
| 77 | +┌─────────────────────┐ |
| 78 | +│ Output.render │ |
| 79 | +│ • determine_style │ |
| 80 | +│ • apply_renderer │ |
| 81 | +│ • format_output │ |
| 82 | +└─────────────────────┘ |
| 83 | + │ |
| 84 | + ┌────┴────┐ |
| 85 | + ▼ ▼ |
| 86 | +┌──────────┐ ┌──────────┐ |
| 87 | +│ Fancy │ │ Plain │ |
| 88 | +│ Renderer │ │ Renderer │ |
| 89 | +└──────────┘ └──────────┘ |
| 90 | +``` |
| 91 | + |
| 92 | +### Module Structure |
| 93 | + |
| 94 | +```elixir |
| 95 | +Arca.Cli.Ctx # Context struct and composition functions |
| 96 | +Arca.Cli.Output # Main rendering pipeline |
| 97 | +Arca.Cli.Output.FancyRenderer # Colored, formatted output |
| 98 | +Arca.Cli.Output.PlainRenderer # No ANSI codes |
| 99 | +Arca.Cli.Output.DumpRenderer # Raw data inspection |
| 100 | +``` |
| 101 | + |
| 102 | +### Data Flow |
| 103 | + |
| 104 | +1. **Command Execution**: Handler returns either `%Ctx{}` or legacy value |
| 105 | +2. **Callback Processing**: `:format_command_result` callback checks return type |
| 106 | +3. **Context Rendering**: If Ctx, render through style pipeline |
| 107 | +4. **Style Selection**: Auto-detect or use explicit style setting |
| 108 | +5. **Output Generation**: Renderer converts structured data to strings |
| 109 | +6. **Display**: Final output sent to appropriate destination |
| 110 | + |
| 111 | +### Context Structure |
| 112 | + |
| 113 | +```elixir |
| 114 | +%Arca.Cli.Ctx{ |
| 115 | + command: atom(), # Command being executed |
| 116 | + args: map(), # Parsed arguments |
| 117 | + options: map(), # Command options |
| 118 | + output: list(), # Structured output items |
| 119 | + errors: list(), # Error messages |
| 120 | + status: atom(), # :ok | :error | :warning |
| 121 | + cargo: map(), # Command-specific data |
| 122 | + meta: map() # Style, format, and other metadata |
| 123 | +} |
| 124 | +``` |
| 125 | + |
| 126 | +### Output Item Types |
| 127 | + |
| 128 | +```elixir |
| 129 | +# Messages with semantic meaning |
| 130 | +{:success, message} |
| 131 | +{:error, message} |
| 132 | +{:warning, message} |
| 133 | +{:info, message} |
| 134 | + |
| 135 | +# Structured data |
| 136 | +{:table, rows, headers: headers} |
| 137 | +{:list, items, title: title} |
| 138 | +{:text, content} |
| 139 | + |
| 140 | +# Interactive elements (fancy mode only) |
| 141 | +{:spinner, label, func} |
| 142 | +{:progress, label, func} |
| 143 | +``` |
| 144 | + |
| 145 | +## Implementation Plan |
| 146 | + |
| 147 | +### Phase 1: Core Infrastructure |
| 148 | + |
| 149 | +1. Add `Arca.Cli.Ctx` module with composition functions |
| 150 | +2. Add `Arca.Cli.Output` module with rendering pipeline |
| 151 | +3. Implement FancyRenderer and PlainRenderer modules |
| 152 | +4. Register `:format_command_result` callback point |
| 153 | +5. Update `execute_command` to handle Ctx returns |
| 154 | + |
| 155 | +### Phase 2: Global Options |
| 156 | + |
| 157 | +1. Add `--style` option to global CLI options |
| 158 | +2. Add `--no-ansi` as alias for `--style plain` |
| 159 | +3. Add environment variable support (NO_COLOR) |
| 160 | +4. Update help text to document new options |
| 161 | + |
| 162 | +### Phase 3: Testing & Documentation |
| 163 | + |
| 164 | +1. Add comprehensive tests for all components |
| 165 | +2. Test backwards compatibility scenarios |
| 166 | +3. Create migration guide for command authors |
| 167 | +4. Add examples of context-based commands |
| 168 | + |
| 169 | +### Phase 4: Validation |
| 170 | + |
| 171 | +1. Migrate a sample command to use Ctx |
| 172 | +2. Verify test fixtures work correctly |
| 173 | +3. Performance testing |
| 174 | +4. Integration testing with existing apps |
| 175 | + |
| 176 | +## Alternatives Considered |
| 177 | + |
| 178 | +### 1. Separate Output Module per Command |
| 179 | + |
| 180 | +- **Rejected**: Too much boilerplate, violates DRY principle |
| 181 | + |
| 182 | +### 2. Middleware Pipeline Approach |
| 183 | + |
| 184 | +- **Rejected**: Over-engineered for the use case, harder to debug |
| 185 | + |
| 186 | +### 3. Direct Integration into BaseCommand |
| 187 | + |
| 188 | +- **Rejected**: Would require changes to all existing commands, breaks compatibility |
| 189 | + |
| 190 | +### 4. External Formatting Library |
| 191 | + |
| 192 | +- **Rejected**: Adds dependency, less control over implementation |
| 193 | + |
| 194 | +## Success Criteria |
| 195 | + |
| 196 | +1. ✅ Existing commands continue working without changes |
| 197 | +2. ✅ New commands can return structured context |
| 198 | +3. ✅ Output automatically adapts to environment (TTY, test, etc.) |
| 199 | +4. ✅ Clean separation between data and presentation |
| 200 | +5. ✅ Easy to add new output types and renderers |
| 201 | +6. ✅ No performance degradation |
| 202 | +7. ✅ Test fixtures can verify output without ANSI codes |
| 203 | + |
| 204 | +## Example Usage |
| 205 | + |
| 206 | +### Legacy Command (Still Works) |
| 207 | + |
| 208 | +```elixir |
| 209 | +def handle(args, settings, optimus) do |
| 210 | + "Simple string output" |
| 211 | +end |
| 212 | +``` |
| 213 | + |
| 214 | +### New Context-Based Command |
| 215 | + |
| 216 | +```elixir |
| 217 | +def handle(args, settings, optimus) do |
| 218 | + Arca.Cli.Ctx.new(args, settings) |
| 219 | + |> process_data() |
| 220 | + |> Ctx.add_output({:success, "Operation completed"}) |
| 221 | + |> Ctx.add_output({:table, rows, headers: ["Name", "Value"]}) |
| 222 | + |> Ctx.complete(:ok) |
| 223 | +end |
| 224 | +``` |
| 225 | + |
| 226 | +### Style Control |
| 227 | + |
| 228 | +```bash |
| 229 | +# Automatic style detection |
| 230 | +mix my.cli command |
| 231 | + |
| 232 | +# Force plain style |
| 233 | +mix my.cli --style plain command |
| 234 | +mix my.cli --no-ansi command |
| 235 | +NO_COLOR=1 mix my.cli command |
| 236 | + |
| 237 | +# Test environment (automatic plain) |
| 238 | +MIX_ENV=test mix my.cli command |
| 239 | +``` |
0 commit comments