bvanhoekelen/terminal-style
Terminal Style is a simple PHP package for styling command-line output with colors, background colors, and text options like bold, underline, and blink. Its chainable API makes it easy to format CLI messages, tables, and Laravel artisan commands with consistent, readable terminal styling.
Install
composer require bvanhoekelen/terminal-style
There is no build step and (as far as the docs show) no required config file. In Laravel the service provider/facade is auto-discovered, so the TerminalStyle alias is available immediately. In a standalone PHP script, require 'vendor/autoload.php' and use the facade or the underlying class from the package namespace (check vendor/bvanhoekelen/terminal-style/src/ for the exact FQCN before assuming).
Where to look first
vendor/bvanhoekelen/terminal-style/README.md) — it is the entire API surface; the package is small.src/TerminalStyle.php — grep the public methods to confirm the exact chainable names (color, background, bold, underline, etc.) and the color keywords accepted. Don't guess method names; the class is ~200 lines and readable in one sitting.config/terminal-style.php is publishable (php artisan vendor:publish --provider=...) — if it exists it only holds defaults, so skip it unless you need to change them.First use case: an Artisan command that is readable at a glance
use TerminalStyle as TS;
public function handle()
{
$this->line(TS::new()->text('Deploying to production…')->yellow()->bold()->toString());
$this->line(TS::new()->text('✔ Migrations applied')->green()->toString());
$this->line(TS::new()->text('✖ Cache warmup failed')->red()->bold()->toString());
}
That is the whole day-to-day value: turning flat $this->line() output into something a human can scan in a CI log.
Builder → string → output. Treat the class as a pure string builder: chain the text and styles, then call toString() (or print()/toArray() if the version you have exposes them) and hand the result to Laravel's output. Keep the builder call at the call site for one-offs, but do not scatter it across a codebase.
Centralise a semantic palette. The biggest maintenance win is never typing color names in business logic. Wrap the package in a small helper in app/Support/:
class Console
{
public static function info(string $m): string { return TS::new()->text($m)->cyan()->toString(); }
public static function success(string $m): string { return TS::new()->text($m)->green()->bold()->toString(); }
public static function warn(string $m): string { return TS::new()->text($m)->yellow()->toString(); }
public static function error(string $m): string { return TS::new()->text($m)->red()->bold()->toString(); }
public static function heading(string $m): string
{
return PHP_EOL . TS::new()->text($m)->white()->bold()->underline()->toString() . PHP_EOL;
}
}
Now $this->line(Console::error($e->getMessage())) replaces $this->error(...) everywhere, and you can swap the implementation later (to Symfony Style, Termwind, or Laravel Prompts) without touching commands. This is the single most important pattern given the package's age.
Blocks and arrays. For multi-line output, build the raw string first with implode(PHP_EOL, $lines) and style the whole block once, rather than styling each line — fewer escape sequences, cleaner output. If you do style per-line (e.g. mixed severities in one report), keep a single trailing reset by styling each element independently rather than nesting calls.
Respect non-decorated output. Style only when the terminal actually supports it:
$styled = $this->output->isDecorated(); // false under --no-ansi, pipe, CI redirect
$this->line($styled ? Console::success($msg) : $msg);
This is the difference between usable and unusable CI logs. Laravel's Symfony OutputInterface already knows; use it.
Pad before you style, never after. For aligned report output:
$row = str_pad($name, 30) . str_pad((string) $count, 8, ' ', STR_PAD_LEFT);
$this->line(TS::new()->text($row)->white()->toString());
Doing it the other way round produces misaligned columns because the ANSI bytes count toward strlen().
Banner / progress-ish headers for long tasks. A styled separator line (str_repeat('─', 72)) plus a bold heading turns a scrolling deploy log into sections. Pair with $this->newLine() rather than embedding newlines in the styled string.
Testing. Assert on the stripped string, not the raw one:
$plain = preg_replace('/\e\[[0-9;]*m/', '', $output);
$this->assertStringContainsString('Migrations applied', $plain);
It is unmaintained. Last release January 2020, ~19 stars, single-author. There is no Symfony 6/7 work, no Laravel 11 promise, and no PHP 8.3 CI. It still works because it is \e[...m string concatenation, but pin the version in composer.json and wrap it (see the palette pattern above) so replacing it with Symfony\Component\Console\Style\SymfonyStyle, Termwind, or Laravel Prompts is a one-file change.
ANSI codes break every string-length operation. strlen, str_pad, substr, mb_strimwidth, table widths, and word-wrapping all see escape bytes. Always measure and truncate the unstyled text, then style. This is the #1 source of "why is my table crooked" bugs.
Order and resets. Chained styles emit escape sequences that can be cancelled by an inner reset — e.g. styling a bold string inside a background block may lose the background after the inner reset. When nesting, apply the outer style last in the chain or build the inner string, strip it, and re-style the composite.
Never put styled output into logs or files. Log::info(Console::error($m)) writes raw escape bytes into laravel.log and every log aggregator. Styling belongs only on the console stream, and only when isDecorated().
Sanitise untrusted strings. Any text interpolated from user input, HTTP responses, or third-party CLI output can already contain \e[ sequences. Strip them (preg_replace('/\e\[[0-9;]*[A-Za-z]/', '', $s)) before styling, or you inherit terminal injection.
Windows and legacy terminals. Truecolor and some attributes degrade or print literal garbage in older cmd.exe. Stick to the 8 basic foreground colors and bold/underline; if you need 256/truecolor, that is a reason to pick a modern alternative.
Debugging escape output. If output looks wrong, inspect the bytes: php artisan cmd | cat -v shows ^[[32m style sequences, and bin2hex($str) reveals what was actually emitted. Toggle --ansi / --no-ansi on the Artisan command to confirm whether the problem is styling or decoration detection.
Extension points are limited. There is no driver, no formatter interface, no middleware. Extend by composition — your Console helper is the extension point. Only subclass if you need a new escape attribute the class doesn't expose (just append the code yourself in toString()).
Version pin. "bvanhoekelen/terminal-style": "^1.0" rather than *; a fork or a breaking change on an abandoned package is unlikely to be announced, and you want composer to fail loudly rather than silently resolve something new.
How can I help you explore Laravel packages today?