Skip to content

Commit f484d55

Browse files
Add configurable console styles for CLI output, including severity backgrounds, syntax highlighting, and semantic tags. Refactor ConsoleStyler and CodeHighlighter to support overrides via config or environment variable. Update documentation with usage examples.
1 parent 982b0e8 commit f484d55

7 files changed

Lines changed: 282 additions & 28 deletions

File tree

.phpunit.result.cache

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Supported environment variables:
3838
- PHP_ERROR_INSIGHT_ROOT: absolute project root to compute relative file paths in the stack (optional)
3939
- PHP_ERROR_INSIGHT_HOST_ROOT: absolute host project root used to map container paths when opening files via editor links (optional; useful in Docker)
4040
- PHP_ERROR_INSIGHT_EDITOR: editor URL template for clickable file links, using %file and %line placeholders (e.g. "vscode://file/%file:%line" or "phpstorm://open?file=%file&line=%line")
41+
- PHP_ERROR_INSIGHT_CONSOLE_COLORS: JSON object to customize console styles (token colors, severity backgrounds, title/suggestions/stack/location).
4142

4243
Configuration examples:
4344

@@ -181,6 +182,79 @@ Technical details:
181182

182183
For more details, see docs/sanitizzazione-dati-ai.md.
183184

185+
## Console colors customization (CLI)
186+
187+
You can customize the colors used in the CLI output (syntax highlighting, severity header background, title, AI suggestions, stack trace and locations).
188+
189+
Two ways to configure:
190+
191+
- Environment variable `PHP_ERROR_INSIGHT_CONSOLE_COLORS` containing a JSON object
192+
- Programmatic via `Config::fromEnvAndArray(['consoleColors' => [...]])`
193+
194+
Structure of the JSON/object:
195+
196+
```json
197+
{
198+
"tokens": {
199+
"default": ["white", null, []],
200+
"comment": ["white", null, []],
201+
"string": ["yellow", null, []],
202+
"keyword": ["magenta", null, ["bold"]],
203+
"html": ["cyan", null, ["bold"]],
204+
"variable": ["cyan", null, []],
205+
"function": ["blue", null, ["bold"]],
206+
"method": ["green", null, ["underscore"]]
207+
},
208+
"severity": {
209+
"error": "red",
210+
"warning": "yellow",
211+
"info": "blue"
212+
},
213+
"styles": {
214+
"title": ["white", null, ["bold"]],
215+
"suggestion": ["green", null, []],
216+
"stack": ["yellow", null, []],
217+
"location": ["blue", null, []],
218+
"gutter_hl": ["white", "red", ["bold"]],
219+
"gutter_num": ["gray", null, []],
220+
"gutter_sep": ["gray", null, []]
221+
}
222+
}
223+
```
224+
225+
Notes:
226+
- Each style uses the form `[fg, bg, options[]]`. `fg` and `bg` accept Symfony Console color names (e.g., "white", "yellow", "red", "blue", "cyan", "magenta", "gray"), and `options` can include `bold`, `underscore`, `blink`, `reverse`, `conceal`.
227+
- `tokens` overrides the syntax highlighter palette for these categories: `default`, `comment`, `string`, `keyword`, `html`, `variable`, `function`, `method`.
228+
- `severity` maps `error|warning|info` to a background color name for the header badge with the severity label.
229+
- `styles` lets you tweak semantic tags used by the renderer: `title` (error message), `suggestion` (AI suggestions label and items), `stack` (stack trace lines when rendered in compact mode), `location` (file:line), `gutter_hl` (current line number in code excerpts), `gutter_num` (non-current line numbers), and `gutter_sep` (the vertical separator).
230+
- Any key you omit falls back to sensible defaults.
231+
232+
Examples
233+
234+
- Via environment variable:
235+
236+
```bash
237+
export PHP_ERROR_INSIGHT_CONSOLE_COLORS='{
238+
"severity": {"error":"magenta","warning":"yellow","info":"cyan"},
239+
"styles": {"title":["white",null,["bold"]],"suggestion":["cyan",null,[]]},
240+
"tokens": {"keyword":["magenta",null,["bold"]],"string":["green",null,[]]}
241+
}'
242+
```
243+
244+
- Via code:
245+
246+
```php
247+
use PhpErrorInsight\Config;
248+
249+
$config = Config::fromEnvAndArray([
250+
'consoleColors' => [
251+
'severity' => [ 'error' => 'magenta', 'warning' => 'yellow', 'info' => 'blue' ],
252+
'styles' => [ 'title' => ['white', null, ['bold']], 'suggestion' => ['cyan', null, []] ],
253+
'tokens' => [ 'keyword' => ['magenta', null, ['bold']], 'string' => ['green', null, []] ],
254+
],
255+
]);
256+
```
257+
184258
## Development
185259

186260
This project uses several development tools to maintain code quality. Use the following composer scripts for easy access to these tools:

examples/vanilla/index.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// Minimal example demonstrating PHP Error Explainer
66
// Run with: php examples/vanilla/index.php
77
// Optional AI config (env):
8-
// PHP_ERROR_INSIGHT_BACKEND=none|local|api|openai|anthropic|google|gemini
8+
// called PHP_ERROR_INSIGHT_BACKEND=none|local|api|openai|anthropic|google|gemini
99
// PHP_ERROR_INSIGHT_MODEL=llama3:instruct|gpt-4o-mini|claude-3-5-sonnet-20240620|gemini-1.5-flash|...
1010
// PHP_ERROR_INSIGHT_API_URL=http://localhost:11434 (Ollama) | https://api.openai.com/v1/chat/completions (OpenAI) | https://api.anthropic.com/v1/messages (Anthropic) | https://generativelanguage.googleapis.com/v1/models (Google Gemini)
1111
// PHP_ERROR_INSIGHT_API_KEY=sk-... (OpenAI) | api-key (Anthropic) | api-key (Google Gemini)
@@ -34,4 +34,5 @@
3434
trigger_error((string) "function called.", E_USER_NOTICE);
3535
trigger_error("function called.", E_USER_DEPRECATED);
3636
trigger_error("function called.", E_USER_WARNING);
37+
@trigger_error((string) "Should skipped.", E_USER_NOTICE); //should skip error
3738
throw new Exception("exception thrown.");

src/Config.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use function array_key_exists;
88
use function in_array;
9+
use function is_array;
910

1011
final class Config
1112
{
@@ -41,6 +42,31 @@ final class Config
4142

4243
public ?string $editorUrl = null; // template like "vscode://file/%file:%line" or "phpstorm://open?file=%file&line=%line"
4344

45+
/**
46+
* Console colors and styles configuration.
47+
* Structure example:
48+
* [
49+
* 'tokens' => [ 'default' => ['white', null, []], ... ],
50+
* 'styles' => [
51+
* 'yellow' => ['yellow', null, []],
52+
* 'green' => ['green', null, []],
53+
* 'blue' => ['blue', null, []],
54+
* 'dim' => ['gray', null, []],
55+
* 'boldwhite' => ['white', null, ['bold']],
56+
* 'gutter_hl' => ['white', 'red', ['bold']],
57+
* 'gutter_num' => ['gray', null, []],
58+
* 'gutter_sep' => ['gray', null, []],
59+
* 'title' => ['white', null, ['bold']],
60+
* 'suggestion' => ['green', null, []],
61+
* 'stack' => ['yellow', null, []],
62+
* 'location' => ['blue', null, []]
63+
* ]
64+
* ].
65+
*
66+
* @var array<string, mixed>|null
67+
*/
68+
public ?array $consoleColors = null;
69+
4470
/**
4571
* @param array<string, mixed> $options
4672
*/
@@ -93,6 +119,10 @@ public function __construct(array $options = [])
93119
if (array_key_exists('editorUrl', $options)) {
94120
$this->editorUrl = null !== $options['editorUrl'] ? (string) $options['editorUrl'] : null;
95121
}
122+
123+
if (array_key_exists('consoleColors', $options)) {
124+
$this->consoleColors = is_array($options['consoleColors']) ? $options['consoleColors'] : null;
125+
}
96126
}
97127

98128
/**
@@ -113,6 +143,16 @@ public static function fromEnvAndArray(array $options = []): self
113143
'projectRoot' => self::getEnvVar('PHP_ERROR_INSIGHT_ROOT') ?: null,
114144
'hostProjectRoot' => self::getEnvVar('PHP_ERROR_INSIGHT_HOST_ROOT') ?: null,
115145
'editorUrl' => self::getEnvVar('PHP_ERROR_INSIGHT_EDITOR') ?: null,
146+
'consoleColors' => (function (): ?array {
147+
$raw = self::getEnvVar('PHP_ERROR_INSIGHT_CONSOLE_COLORS');
148+
if (false === $raw || '' === $raw || '0' === $raw) {
149+
return null;
150+
}
151+
152+
$decoded = json_decode($raw, true);
153+
154+
return is_array($decoded) ? $decoded : null;
155+
})(),
116156
];
117157
// Options override env
118158
$merged = array_merge($env, $options);

src/Internal/CodeHighlighter.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,24 @@ final class CodeHighlighter
5353
* pe-tok-string, pe-tok-comment, pe-tok-keyword, pe-tok-default, pe-tok-html
5454
* plus: pe-tok-variable, pe-tok-function, pe-tok-method
5555
*/
56-
public function registerStyles(OutputFormatterInterface $formatter, string $theme = self::THEME_DEFAULT): void
56+
/**
57+
* @param array<string, array{0?:string,1?:string|null,2?:array<string>}>|null $overrides
58+
*/
59+
public function registerStyles(OutputFormatterInterface $formatter, string $theme = self::THEME_DEFAULT, ?array $overrides = null): void
5760
{
5861
$palette = $this->paletteFor($theme);
62+
if (is_array($overrides)) {
63+
// Merge overrides into palette (shallow per key)
64+
foreach ($overrides as $k => $spec) {
65+
if (is_array($spec)) {
66+
$palette[$k] = [
67+
$spec[0] ?? ($palette[$k][0] ?? 'white'),
68+
$spec[1] ?? ($palette[$k][1] ?? null),
69+
$spec[2] ?? ($palette[$k][2] ?? []),
70+
];
71+
}
72+
}
73+
}
5974

6075
$formatter->setStyle('pe-tok-string', new OutputFormatterStyle(
6176
$palette['string'][0] ?? 'green',

src/Internal/ConsoleStyler.php

Lines changed: 110 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
88
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
99

10+
use function is_array;
11+
use function is_string;
12+
1013
/**
1114
* ConsoleStyler defines style tags and helps produce tagged strings.
1215
*
@@ -16,19 +19,71 @@ final class ConsoleStyler
1619
{
1720
/**
1821
* Register our custom styles into a Symfony Output formatter.
22+
*
23+
* @param array<string,mixed>|null $config styles override structure (Config::$consoleColors)
1924
*/
20-
public function registerStyles(OutputFormatterInterface $formatter): void
25+
public function registerStyles(OutputFormatterInterface $formatter, ?array $config = null): void
2126
{
22-
$formatter->setStyle('pe-yellow', new OutputFormatterStyle('yellow'));
23-
$formatter->setStyle('pe-green', new OutputFormatterStyle('green'));
24-
$formatter->setStyle('pe-blue', new OutputFormatterStyle('blue'));
25-
$formatter->setStyle('pe-dim', new OutputFormatterStyle('gray'));
26-
$formatter->setStyle('pe-boldwhite', new OutputFormatterStyle('white', null, ['bold']));
27-
$formatter->setStyle('pe-header-red', new OutputFormatterStyle('white', 'red', ['bold']));
28-
$formatter->setStyle('pe-header-yellow', new OutputFormatterStyle('white', 'yellow', ['bold']));
29-
$formatter->setStyle('pe-header-blue', new OutputFormatterStyle('white', 'blue', ['bold']));
30-
$formatter->setStyle('pe-gutter-hl', new OutputFormatterStyle('white', 'red', ['bold']));
27+
$styles = is_array($config) && isset($config['styles']) && is_array($config['styles']) ? $config['styles'] : [];
28+
29+
// Helper to read [fg, bg, options[]]
30+
$spec = static function (array $styles, string $key, array $def): array {
31+
$v = $styles[$key] ?? null;
32+
if (!is_array($v)) {
33+
return $def;
34+
}
35+
36+
return [
37+
(string) ($v[0] ?? $def[0]),
38+
null !== ($v[1] ?? null) ? (string) $v[1] : ($def[1] ?? null),
39+
is_array($v[2] ?? null) ? $v[2] : ($def[2] ?? []),
40+
];
41+
};
42+
43+
// Core text styles (defaults preserve current behavior)
44+
[$fg, $bg, $opt] = $spec($styles, 'yellow', ['yellow', null, []]);
45+
$formatter->setStyle('pe-yellow', new OutputFormatterStyle($fg, $bg, $opt));
46+
[$fg, $bg, $opt] = $spec($styles, 'green', ['green', null, []]);
47+
$formatter->setStyle('pe-green', new OutputFormatterStyle($fg, $bg, $opt));
48+
[$fg, $bg, $opt] = $spec($styles, 'blue', ['blue', null, []]);
49+
$formatter->setStyle('pe-blue', new OutputFormatterStyle($fg, $bg, $opt));
50+
[$fg, $bg, $opt] = $spec($styles, 'dim', ['gray', null, []]);
51+
$formatter->setStyle('pe-dim', new OutputFormatterStyle($fg, $bg, $opt));
52+
[$fg, $bg, $opt] = $spec($styles, 'boldwhite', ['white', null, ['bold']]);
53+
$formatter->setStyle('pe-boldwhite', new OutputFormatterStyle($fg, $bg, $opt));
3154

55+
// Title, Suggestion, Stack, Location semantic tags (new; default map to old colors)
56+
[$fg, $bg, $opt] = $spec($styles, 'title', ['white', null, ['bold']]);
57+
$formatter->setStyle('pe-title', new OutputFormatterStyle($fg, $bg, $opt));
58+
[$fg, $bg, $opt] = $spec($styles, 'suggestion', ['green', null, []]);
59+
$formatter->setStyle('pe-suggestion', new OutputFormatterStyle($fg, $bg, $opt));
60+
[$fg, $bg, $opt] = $spec($styles, 'stack', ['yellow', null, []]);
61+
$formatter->setStyle('pe-stack', new OutputFormatterStyle($fg, $bg, $opt));
62+
[$fg, $bg, $opt] = $spec($styles, 'location', ['blue', null, []]);
63+
$formatter->setStyle('pe-location', new OutputFormatterStyle($fg, $bg, $opt));
64+
65+
// Gutter styles
66+
[$fg, $bg, $opt] = $spec($styles, 'gutter_hl', ['white', 'red', ['bold']]);
67+
$formatter->setStyle('pe-gutter-hl', new OutputFormatterStyle($fg, $bg, $opt));
68+
[$fg, $bg, $opt] = $spec($styles, 'gutter_num', ['gray', null, []]);
69+
$formatter->setStyle('pe-gutter-num', new OutputFormatterStyle($fg, $bg, $opt));
70+
[$fg, $bg, $opt] = $spec($styles, 'gutter_sep', ['gray', null, []]);
71+
$formatter->setStyle('pe-gutter-sep', new OutputFormatterStyle($fg, $bg, $opt));
72+
73+
// Severity backgrounds: build tags like pe-header-<color>
74+
$sev = is_array($config) && isset($config['severity']) && is_array($config['severity']) ? $config['severity'] : [];
75+
$bgColors = ['red', 'yellow', 'blue'];
76+
foreach (['error', 'warning', 'info'] as $k) {
77+
$v = $sev[$k] ?? null;
78+
if (is_string($v) && '' !== $v) {
79+
$bgColors[] = strtolower($v);
80+
}
81+
}
82+
83+
$bgColors = array_values(array_unique($bgColors));
84+
foreach ($bgColors as $c) {
85+
$formatter->setStyle('pe-header-' . $c, new OutputFormatterStyle('white', $c, ['bold']));
86+
}
3287
}
3388

3489
private function tag(string $name, string $s): string
@@ -82,4 +137,49 @@ public function gutterHighlight(string $s): string
82137
{
83138
return $this->tag('pe-gutter-hl', $s);
84139
}
140+
141+
/**
142+
* Default gutter line number (non-highlighted lines).
143+
*/
144+
public function gutterNumber(string $s): string
145+
{
146+
return $this->tag('pe-gutter-num', $s);
147+
}
148+
149+
/**
150+
* Gutter vertical separator style.
151+
*/
152+
public function gutterSeparator(string $s): string
153+
{
154+
return $this->tag('pe-gutter-sep', $s);
155+
}
156+
157+
// Convenience wrappers for semantic styles
158+
public function title(string $s): string
159+
{
160+
return $this->tag('pe-title', $s);
161+
}
162+
163+
public function suggestion(string $s): string
164+
{
165+
return $this->tag('pe-suggestion', $s);
166+
}
167+
168+
public function stack(string $s): string
169+
{
170+
return $this->tag('pe-stack', $s);
171+
}
172+
173+
public function location(string $s): string
174+
{
175+
return $this->tag('pe-location', $s);
176+
}
177+
178+
/**
179+
* Bold white text on named background color (e.g., 'red','yellow','blue', ...).
180+
*/
181+
public function headerOnBgName(string $bgName, string $s): string
182+
{
183+
return $this->tag('pe-header-' . strtolower($bgName), $s);
184+
}
85185
}

0 commit comments

Comments
 (0)