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