hoa/console
Hoa\Console is a PHP library for building command-line applications. It provides terminal abstraction, cursor and window control, readline-style input, process management, option parsing, and I/O helpers, making rich interactive CLI tools easier to develop and maintain.
hoa/console is a low-level terminal toolkit (readline, cursor control, tty introspection, process spawning) — not an Artisan/Symfony Console replacement. It is archived (last release May 2017), so treat it as a legacy dependency you inherit or opt into deliberately, not something to build a new Laravel app around.
Install and confirm it actually loads on your PHP version before writing any code:
composer require hoa/console:^3.17
php -r "require 'vendor/autoload.php'; var_dump(class_exists('Hoa\Console\Console'));"
Where to look first, in order:
Hoa\Console\Console::getInstance() — the singleton giving you getInput(), getOutput(), getErrorOutput().Hoa\Console\Readline\Readline — the reason most people pull this in (history, autocompletion, multi-line input).Hoa\Console\Cursor and Hoa\Console\Window — ANSI cursor movement and terminal sizing.Hoa\Console\Chrome\Text::columnize() — cheap column/table formatting.Hoa\Console\Processus — the old process runner; you almost certainly want Illuminate\Support\Facades\Process instead.First use case — a genuinely interactive prompt inside an Artisan command, which Laravel's $this->ask() does poorly:
use Hoa\Console\Readline\Readline;
public function handle()
{
if (! $this->input->isInteractive()) {
$this->error('Requires an interactive terminal.');
return self::FAILURE;
}
$readline = new Readline();
$readline->addMapping("\t", fn ($line) => /* completion logic */ $line);
$readline->setAutocompleter(new MyCompleter());
$answer = $readline->readLine('Project name> ');
}
Reality check before you start: verify it runs on your PHP version (8.x is not supported upstream) and that registering it doesn't clash with Symfony Console's own terminal handling. If the smoke test throws deprecations or fatals, stop — the package is unmaintained and patches are on you.
Isolate it behind a small adapter. Never scatter Hoa\Console\* calls through commands; wrap them in an injectable class so you can swap in laravel/prompts or Symfony helpers later, and so tests can fake it.
final class Terminal
{
public function isInteractive(): bool
{
return Hoa\Console\Tty::isatty(STDOUT)
&& app()->runningInConsole()
&& ! app()->runningUnitTests();
}
public function prompt(string $label): ?string { /* Readline */ }
public function clearLine(): void { /* Cursor::clear('line') */ }
}
Gate every escape sequence behind a decoration check. Hoa emits raw ANSI with no awareness of Laravel's --no-ansi, NO_COLOR, or redirected output. Build one predicate and reuse it:
$decorated = $this->output->isDecorated() && $this->terminal->isInteractive();
Use Readline only for single, foreground, human-facing prompts. It blocks on STDIN, maintains its own history file, and takes over the terminal. That makes it a poor fit inside queued jobs, artisan invoked from another process, PHPUnit, or anything piped (echo "x" | artisan ...), where it will hang or read garbage. Guard with isInteractive() and provide a --non-interactive flag path everywhere.
Cursor/Window tricks belong in a "live" render loop, not in normal output. Spinners, in-place progress counters, and resizing-aware dashboards work like this:
use Hoa\Console\Cursor;
use Hoa\Console\Window;
$width = Window::getSize()['x'] ?? 80;
$height = Window::getSize()['y'] ?? 24;
Cursor::save();
foreach ($items as $i => $item) {
Cursor::move('up'); // reposition
Cursor::clear('line');
echo substr($item, 0, $width);
usleep(50_000);
}
Cursor::restore();
Note the ?? 80 — Window::getSize() returns null/empty when there's no real tty (CI, Docker without -t, IDE run panels), so always default.
Prefer Chrome\Text::columnize() only when Symfony's $this->table() is inconvenient — e.g. when you need a raw string for a Log::info block or an email body rather than console output. Symfony's table helper handles borders, wrapping, and styling better, and it respects --no-ansi automatically.
Do not use Processus in new code. Laravel 10+ ships the Process facade with timeouts, retries, output streaming, and fakes. If a legacy code path is already using Processus, migrate it rather than extending it — Hoa's process handling predates proc_open argument-array safety and is a quoting/injection risk on Windows.
Integration tip — service provider. Because Hoa packages register stream wrappers and autoload files via Composer files, bind your adapter as a singleton in a provider and resolve lazily. Doing the Console::getInstance() bootstrap in register() can capture STDIN before Artisan has finished setting up its IO.
hoa/consistency and hoa/stream, and possibly fatals from reflection/each()-era code. Test in a throwaway container first; if it errors, drop it rather than patching vendor code.symfony/console. Mixing both in one command produces duplicated prompts, eaten keystrokes, or leftover escape codes. Pick one per command.Console::getInstance(), Cursor, and Readline all talk to process-wide STDIN/STDOUT. That makes them untestable and unsafe under parallel test runs or artisan invoked recursively. Your adapter should be the only seam that touches them.Tty::isatty(), Window::getSize(), and interactive Readline all fail or degrade silently when output is piped, in CI, in Docker without -it, or inside phpunit. Always provide a non-interactive branch and default terminal dimensions.NO_COLOR / --no-ansi are ignored. Hoa has no concept of decoration negotiation. Honour $this->output->isDecorated(), $this->option('no-ansi'), and getenv('NO_COLOR') yourself before emitting any Cursor::colorize() output — otherwise your logs and CI artifacts fill with \e[31m.cat -v to see what's actually emitted, and script -q /dev/null artisan cmd to fake a tty. Leftover artifacts almost always mean a missing Cursor::clear() or an early exit mid-render-loop — wrap render loops in try/finally with Cursor::restore().readline, ext-posix, ext-pcntl, TERM). TERM=dumb is not respected; you must check it yourself.Readline::addMapping() / setAutocompleter() and the hoa:// stream wrappers. There's no DI, no events worth hooking, and no upstream to send fixes to — treat the API as frozen.hoa:// wrappers at autoload time; in rare cases that collides with other stream-wrapper users (flysystem adapters, phar handling). If you see bizarre include failures after adding it, suspect this first.Readline experience or legacy code you can't yet remove. For prompts use laravel/prompts; for colours and progress use Symfony's Style/ProgressBar; for tables use $this->table(); for processes use the Process facade. Budget for a removal, not an adoption.How can I help you explore Laravel packages today?