Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,53 @@ All notable changes to `laranail/console` are documented in this file.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **A fluent API on `ConsoleUIFormatter`.** You can now build a whole style as one chain:

```php
ConsoleUIFormatter::create()->txtColorRed()->bgColorWhite()->bold()
->message('✓ Deployed 🚀')->padding(2)->lineHeight(3)
->addSpaceBefore()->addSpaceAfter(1, ConsoleUIFormatter::TAB);
```

- **Colours:** `txtColor<Name>()` and `bgColor<Name>()` for 38 named colours, each declared
as an `@method`. `fg()` and `bg()` take any colour `Support\Color` parses.
- **Text:** `message()` resolves emoji shortcodes through `Support\Emoji`, which uses
`laranail/emojis` when it is installed. `icon()` adds an emoji in front of the message.
Names resolve at render time, so `capabilities()` applies wherever it sits in the chain.
`bold()`, `underline()`, `blink()`, `reverse()`, `conceal()` and `href()` set the style.
- **Layout:** `padding()` adds space inside the background, and `lineHeight()` adds blank
background lines above and below. `addSpaceBefore()` and `addSpaceAfter()` add spaces or
tabs outside it; each count must be positive, and repeated calls add up.
- **Output:** `write()` renders straight to an output, and `toAnsi()` returns raw ANSI.
- **Control:** `markup()` lets tags inside the message act as markup, and `capabilities()`
pins emoji and colour support.

The existing methods are unchanged, and a plain message renders exactly as before. See
`docs/tools/formatting.md` and `examples/tools/formatting.php`.

### Fixed

- **The 14 `BRIGHT_*` and `BG_BRIGHT_*` constants threw when written.** The colour sanitiser
stripped the hyphen, so `bright-red` reached Symfony as `brightred`.
- **`colorize()` emitted no colour for the `BRIGHT_*` constants**, because it looked them up
as `bright-red` in a map keyed `bright_red`.
- **Colour names that `Support\Color` accepts** (`orange`, `slate`, `crimson`, …) threw when
written. They are now converted to hex when set. An unknown colour throws
`InvalidColorException` at the call that sets it, not later when the string is written.
- **An unknown text style**, such as `italic`, which Symfony does not support, now throws when
it is set rather than when the string is written.
- **A clickable text colour lost its colour.** The link path returned before adding `fg`/`bg`.

### Changed

- **Message text is escaped.** A message containing `</>` used to end the style early, and
`<fg=red>` inside it took effect. Both now print literally. Call `markup()` to keep the old
behaviour for trusted markup.

## [0.1.3] - 2026-09-26

### Added
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"php examples/tools/barchart.php > /dev/null",
"php examples/tools/charts.php > /dev/null",
"php examples/tools/emoji.php > /dev/null",
"php examples/tools/formatting.php > /dev/null",
"php examples/tools/symbols.php > /dev/null",
"php examples/tools/writing.php > /dev/null 2>&1",
"php examples/tools/interactive.php > /dev/null",
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Console
├── Providers\ConsoleServiceProvider # config + lang + registers child providers
├── Exceptions\ConsoleException # base, fromKey() with safe fallback
├── Tools\ # OUTPUT
│ ├── Formatting\ # ConsoleUIFormatter (colour/badge/link primitives)
│ ├── Formatting\ # ConsoleUIFormatter (fluent markup builder: colour/emoji/badge/link/layout)
│ ├── Widgets\ # Spinner, ProgressBar, Box, Tree, Table, TaskProgress,
│ │ # Summary, Header, Banner, Panel/PanelBlock, Menu\…
│ ├── Contracts\ # Renderable, Interactive (panel composition / live)
Expand Down
121 changes: 100 additions & 21 deletions docs/tools/formatting.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,119 @@ the shared [Capabilities](support.md#capabilities).
> context statuses and emoji), see the [Console writer](writing.md)
> (`Console::writer()`). `ConsoleUIFormatter` returns strings; `ConsoleWriter` writes them.

## Two output modes
## Fluent builder

- **Markup** — `success()`, `error()`, `warning()`, `info()`, `format()`,
`badge()`, `badges()`, `link()`, `hex()` return Symfony Console markup
(`<fg=green>…</>`). Write them **through an output** so colour renders
(echoing prints literal tags). (`render()` is an *instance* method on a
configured formatter, not one of these static helpers.)
Every method returns the builder, so a whole style reads as one chain:

```php
use Simtabi\Laranail\Console\Tools\Formatting\ConsoleUIFormatter;
```php
use Simtabi\Laranail\Console\Tools\Formatting\ConsoleUIFormatter;

$text = ConsoleUIFormatter::create()
->txtColorRed()->bgColorWhite()->bold()
->message('✓ Deployed 🚀')
->padding(2) // inside the background
->lineHeight(3) // blank background lines above and below
->addSpaceBefore(2) // outside the background
->addSpaceAfter(1, ConsoleUIFormatter::TAB);

$text->write($output); // or $output->writeln((string) $text)
```

The builder is `Stringable`. Pass it wherever `string|Stringable` is accepted.
Where a plain `string` parameter is declared, cast it with `(string)`: under
`strict_types`, Symfony's `writeln()` rejects the object itself.

| Method | Does |
|---|---|
| `message(string\|Stringable)` | Sets the text. `:shortcodes:` resolve through [`Emoji`](support.md#emoji). |
| `addMessage(string)` | Sets the text exactly as given, without shortcode resolution. |
| `icon(string $name)` | Puts an emoji, by name, in front of the message with one space. An unknown name adds nothing. |
| `txtColor<Name>()` / `bgColor<Name>()` | Foreground or background from a named colour, e.g. `txtColorBrightRed()`, `bgColorSlate()`. |
| `fg(string)` / `bg(string)` | Any colour: a Symfony name, a [`Color`](colors.md) name, `#hex`, `rgb()`, `hsl()` or `@N`. |
| `bold()`, `underline()`, `blink()`, `reverse()`, `conceal()` | Text options. `addTextStyles()` takes the same names, plus the `underline` and `hidden` aliases. |
| `padding(int)` | Spaces either side of the message, inside its background. |
| `lineHeight(int)` | Total height in lines. The message sits in the middle; the odd extra line goes below. |
| `addSpaceBefore(int = 1, string = SPACE)` | Whitespace before the message, outside its background. Pass `ConsoleUIFormatter::TAB` for tabs. Calls add up. |
| `addSpaceAfter(int = 1, string = SPACE)` | The same, after the message. |
| `href(string)` | Makes the text a link. The URL goes through the [`Hyperlink`](support.md#hyperlink) scheme allow-list. |
| `markup(bool = true)` | Treats formatter tags inside the message as markup. Off by default. |
| `capabilities(Capabilities)` | Pins the emoji fallback and `toAnsi()` instead of detecting the terminal. |
| `render()` / `(string)` | Symfony markup, one tag per line. |
| `write(OutputInterface)` | Renders and writes. |
| `toAnsi(?Capabilities)` | Raw ANSI, ready to `echo`. Plain text without colour support. |
| `reset()` | Clears everything, layout and spacing included. |

### Names for `txtColor<Name>()` and `bgColor<Name>()`

There are 38 names, each available both as `txtColor<Name>()` and as `bgColor<Name>()`.

- **Symfony's own palette (17).** These pass through as names, so they follow the terminal's theme:
`Black`, `Red`, `Green`, `Yellow`, `Blue`, `Magenta`, `Cyan`, `White`, `Default`, `Gray`,
and `BrightRed` through `BrightWhite`.
- **The [`Color`](colors.md) palette (21).** These are sent as hex:
`Lime`, `Grey`, `Silver`, `Maroon`, `Olive`, `Navy`, `Teal`, `Purple`, `Orange`, `Pink`,
`Brown`, `Gold`, `Slate`, `Indigo`, `Violet`, `Crimson`, `Coral`, `Salmon`, `Turquoise`,
`Aqua`, `Mint`.

Each one is declared as an `@method`, so IDEs complete them. `SYMFONY_COLORS` lists the
names that pass through.

### Errors surface where they are set

An unknown colour throws `InvalidColorException` when you set it. An unknown text style,
`padding(-1)`, `lineHeight(0)`, or a space count below 1 throws
`InvalidArgumentException`. Symfony's formatter would otherwise throw only when the
string is finally written, far from the call that caused it.

### Emoji

`message()` and `icon()` resolve names through [`Emoji`](support.md#emoji). It serves
every Unicode shortcode when the optional `laranail/emojis` package is installed,
and console's built-in map otherwise. On a terminal without Unicode it falls back to
ASCII (`🚀` becomes `->`). Widths are measured per grapheme, so a two-column emoji
still lines up with `padding()` and `lineHeight()`.

Names resolve when the string is rendered, not when `message()` or `icon()` is called,
so `capabilities()` applies wherever it sits in the chain. With `laranail/emojis`
installed, a name only its catalogue knows (`:unicorn:`) resolves too; console's own
map still wins for the names it defines, as in `Emoji`.

```bash
composer require laranail/emojis # optional: the full Unicode catalogue
```

### Message text is escaped

Tags inside the message are shown literally, so a message carrying `</>` or
`<fg=red>` cannot end or change the style. Call `markup()` when the message is
trusted markup of your own.

## Static helpers

- **Markup**: `success()`, `error()`, `warning()`, `info()`, `format()`,
`badge()`, `badges()`, `link()` and `hex()` return Symfony Console markup
(`<fg=green>…</>`). Write them **through an output** so the colour renders;
echoing them prints the tags literally.

```php
$output->writeln(ConsoleUIFormatter::success('Done!'));
$output->writeln(ConsoleUIFormatter::badge('NEW', ConsoleUIFormatter::BADGE_STYLE_SUCCESS));
$output->writeln(ConsoleUIFormatter::link('Docs', 'https://example.com')); // OSC-8, scheme allow-listed

$output->writeln(
ConsoleUIFormatter::create()
->addMessage('Processing')
->addTextColor(ConsoleUIFormatter::GREEN)
->addTextStyles(ConsoleUIFormatter::BOLD)
->render()
);
```

- **Raw ANSI (echo-safe)** — `colorize()` emits a ready-to-print coloured string:
- **Raw ANSI (echo-safe)**: `colorize()` emits a coloured string ready to print. It
accepts the `BRIGHT_*` constants.

```php
echo ConsoleUIFormatter::create()->colorize('OK', ConsoleUIFormatter::GREEN, bold: true);
```

Colour/style/badge constants (`GREEN`, `RED`, `BOLD`, `BADGE_STYLE_*`, …) and the
`ANSI_COLORS` map are public on the class. `getBadgeStyles()` lists the badge
styles. All input is sanitised of terminal control characters via
`sanitizeText()`.
The colour, style and badge constants (`GREEN`, `BRIGHT_RED`, `BOLD`, `BADGE_STYLE_*`,
`SPACE`, `TAB`, and so on) and the `ANSI_COLORS` map are public on the class.
`getBadgeStyles()` lists the badge styles. All input is stripped of terminal control
characters by `sanitizeText()`.

A runnable tour is in `examples/tools/formatting.php`.

## Progress, reports, status lines

Expand Down
3 changes: 3 additions & 0 deletions docs/tools/support.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ Console::emoji()->strip('Done :tada:'); // 'Done' (for plain logs)
leaves unknown `:shortcodes:` untouched; `with()` adds/overrides
(`[unicode, ascii]` or a single string); `has()`/`all()` introspect the set.

`ConsoleUIFormatter::message()` and `icon()` resolve names through this same helper, so the
formatter gets the catalogue and the ASCII fallback too; see [Formatting](formatting.md#emoji).

## Symbols

One Unicode↔ASCII glyph map (status icons + tree connectors), chosen once from
Expand Down
46 changes: 46 additions & 0 deletions examples/tools/formatting.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

/*
* ConsoleUIFormatter: the fluent markup builder -- colour methods, emoji, padding,
* line height and outer spacing -- written through a real Symfony output.
*
* php examples/tools/formatting.php
*/

require __DIR__ . '/../../vendor/autoload.php';

use Symfony\Component\Console\Output\ConsoleOutput;
use Simtabi\Laranail\Console\Tools\Formatting\ConsoleUIFormatter;

$output = new ConsoleOutput;

// Stringable: cast it where a plain string is declared, or call write($output).
$output->writeln(
(string) ConsoleUIFormatter::create()
->txtColorRed()->bgColorWhite()->bold()
->message('✓ Deployed 🚀')
->padding(2)
->lineHeight(3)
->addSpaceBefore(2)
->addSpaceAfter(),
);

$output->writeln('');

// Emoji by name, and :shortcodes: in the message, with an ASCII fallback.
ConsoleUIFormatter::create()->txtColorBrightGreen()->icon('rocket')->message('Shipped :tada:')->write($output);

// Any colour Support\Color understands: names, hex, rgb(), hsl(), @N.
ConsoleUIFormatter::create()->fg('#7c3aed')->bgColorSlate()->padding(1)->message('hex on slate')->write($output);

// Tags inside the message are text, unless markup() says otherwise.
ConsoleUIFormatter::create()->txtColorCyan()->message('literal: </> <fg=red>')->write($output);
ConsoleUIFormatter::create()->message('markup: <fg=red>red</> plain')->markup()->write($output);

// Indent with a tab, badge, and raw ANSI for echo.
ConsoleUIFormatter::create()->addSpaceBefore(1, ConsoleUIFormatter::TAB)->addMessage('new')
->isBadge(ConsoleUIFormatter::BADGE_STYLE_SUCCESS)->write($output);

echo ConsoleUIFormatter::create()->txtColorOrange()->message('echoed as ANSI')->toAnsi(), "\n";
Loading
Loading