nunomaduro/tailcli
A Laravel package from Nuno Maduro that adds an elegant Artisan tail command for streaming application logs in real time. View live log output in your terminal with a clean, developer-friendly CLI experience, making debugging and monitoring simpler.
Install (requires PHP 8.1+, mbstring; the package is published under the Termwind\ namespace):
composer require nunomaduro/tailcli
First render — the entire public surface starts with one function:
use function Termwind\render;
render('<div class="px-2 py-1 bg-green-500 text-white font-bold">Deploy finished</div>');
Where to look first
README → "Usage" — the utility-class list is the vocabulary; there is no CSS reference to learn.src/Termwind.php — the static entry points: render(), renderUsing(), style(), createElement(), parse(), width().src/Elements/ — the tags you can actually use (div, span, p, a, hr, br, ul, ol, li, strong, em, pre, code, dl/dt/dd).src/ValueObjects/ — how colors, spacing and widths are resolved when a class doesn't seem to do anything.First use case — replacing $this->info() in an Artisan command with a padded result block:
class SyncCommand extends Command
{
protected $signature = 'sync';
public function handle(): int
{
// ... work ...
render(<<<'HTML'
<div class="px-2 py-1 bg-blue-900 text-white">
<span class="font-bold">Synced</span> 1,204 records
</div>
<div class="mt-1 text-gray-500">Took 3.2s</div>
HTML, $this->output);
return self::SUCCESS;
}
}
Passing $this->output is the important detail: output then respects --quiet, --no-ansi, and Artisan's test buffering.
Compose output with small PHP helpers. The package has no component system, so your own functions become your components:
function badge(string $text, string $color = 'green'): string
{
return sprintf(
'<span class="px-1 bg-%s-500 text-white">%s</span>',
$color,
Termwind::escape($text)
);
}
render('<div class="ml-1">' . badge('PASS') . ' migrations</div>');
Register named styles once in a service provider's boot() instead of repeating utility soup at every call site:
Termwind::style('ok')->apply('bg-green-500 text-white px-1');
Termwind::style('muted')->apply('text-gray-500');
render('<span class="ok">PASS</span> <span class="muted">2.4s</span>');
Use inline style="..." for one-offs. Inline declarations beat classes and are handy for dynamically computed spacing (style="margin-left: {$n}") where a class can't exist. Spacing values are in terminal cells, not rem.
Two-column rows with flex instead of hand-padding:
render(sprintf(
'<div class="w-full flex justify-between"><span>%s</span><span class="text-green-500">%s</span></div>',
e($label),
e($value)
), $this->output);
Lists for grouped output — <ul> with <li class="ml-2"> renders bullets correctly across terminals, whereas manual - prefixes misalign with wide characters.
Custom elements for a tag you use everywhere: Termwind::createElement() lets you register a name that expands into your markup, keeping command code readable. It's the sanctioned extension point — reach for it before copy-pasting HTML strings across twenty commands.
Testing. Two options: swap the renderer with Termwind::renderUsing(...) to capture output, or assert on the parsed tree:
$output = Termwind::parse('<div class="mt-1">Hello</div>')->toString();
expect($output)->toContain('Hello');
Where it earns its keep in a Laravel app: long-running Artisan commands, scheduled-task reports, about-style diagnostics, installer/onboarding flows, and worker status lines. Keep these helpers in something like app/Console/Support/Output.php so they're testable without booting a command.
Termwind::escape() (or htmlspecialchars) — a stray <, &, or unclosed tag silently swallows the rest of your markup. This is the number-one source of "my output disappeared".<div>s can shift px-*/ml-* results. Keep markup flush-left or trim() it.w-full is the terminal width at render time. When output is piped (php artisan sync | cat) or run in CI, width can collapse to 80 or 0. Guard with Termwind::width() or an isDecorated() check.--ansi or $this->output->setDecorated(true) when you need to eyeball formatting.strlen; rely on the package's width handling rather than substr/str_pad.text-red-500 text-green-500 in one class attribute has no defined winner — pick one, or use inline style and know it wins over classes.render() call reparses HTML; build the full string and emit a single call for batch summaries.render() echoes. When you need the string for a log file or an assertion, use Termwind::parse($html)->toString().handle() works but is wasted work and can surprise other commands in the same process — put them in a provider.note(), table(), confirm()) rather than hand-rolling input handling.Termwind::width(), and remember nested <div>s with px-* inside a w-full parent inflate total width and cause unexpected wrapping.How can I help you explore Laravel packages today?