prateekbhujel / php-terminal
Native terminal primitives for PHP CLI on Unix and Windows
Package info
github.com/prateekbhujel/php-terminal
Language:C
Type:php-ext
Ext name:ext-terminal
pkg:composer/prateekbhujel/php-terminal
Requires
- php: >=8.1
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
terminal for PHP CLI
Native terminal primitives for PHP CLI on Unix-like systems and Windows.
terminal is a PHP extension for terminal capabilities on Unix-like systems and Windows.
It exposes the pieces that are awkward to normalize in userland, especially once Windows enters the picture, without trying to become a full TUI toolkit: TTY detection, ANSI/VT support, terminal sizing, raw mode, key reads, secret input, direct writes, and safe restore handling.
Created and maintained by Pratik Bhujel.
Current release: v1.0.0.
Install
pie install prateekbhujel/php-terminal:^1.0 php --ri terminal
For Windows, download the DLL zip matching your PHP version, architecture and
TS/NTS mode from the release page.
Copy php_terminal.dll to that PHP installation's extension directory and enable
extension=php_terminal.dll in its php.ini. Check the selected runtime with
php --ini and php -i; XAMPP, MAMP, Laragon and other distributions each have
their own PHP configuration.
PHP 8.1 or later is required. CI covers Linux PHP 8.1–8.5, macOS PHP 8.4, and Windows x64 PHP 8.2–8.5 in TS/NTS builds. PHP nightly is checked separately and is advisory. See compatibility for platform boundaries.
A terminal session
use Io\Terminal\Key; use Io\Terminal\Terminal; $terminal = Terminal::create(); // Standard input and standard output. $size = $terminal->getSize(); if ($size !== false) { echo "{$size->cols} columns, {$size->rows} rows\n"; } $key = $terminal->readKey(1.0); if ($key === Key::Up) { echo "Up\n"; }
Io\Terminal\Terminal is the API for new integrations. Its public signatures,
enums and documented behavior are the stable 1.x contract. Compatible additions
and bug fixes may ship in 1.x; incompatible API changes require a new major
release. Experimental PHP nightly behavior is outside that compatibility promise.
The extension supplies terminal operations, not a TUI toolkit. Frameworks must
opt in; installing it does not automatically change Symfony Console or Laravel
Prompts. Native input avoids spawning stty or bundling a hidden-input executable.
An RFC for PHP core is planned; this extension remains independently usable.
Input and output streams
$terminal = Terminal::fromStreams(STDIN, STDERR); $duplex = Terminal::fromStream($stream);
create()and its aliasopen()use standard input and standard output. They do not open a separate controlling terminal.fromStreams($input, $output = null)uses$inputfor both directions when output is omitted or null. Supply an output explicitly for an input-only stream.fromStream($stream)uses one stream for both directions.new Terminal()uses standard input/output; with one non-null input, output defaults to that input. Stream values may be PHP resources orIo\Terminal\Streamenum cases.getInputStream()andgetOutputStream()return those values;getStream()returns the output value on an initialized session.
readKey(), readSecret() and raw mode operate on input. isTty(), dimensions,
ANSI/color operations, title, bell and writes operate on output. Use
stream_isatty($input) to check a resource used for input.
Native input requires a TTY descriptor or Windows console input handle. Pipes
and memory streams are useful outputs but cannot supply native terminal events.
write() and explicit prompts support writable PHP streams, including wrappers.
Hidden input
$terminal = Terminal::fromStreams(STDIN, STDERR); try { $secret = $terminal->readSecret('Password: '); $terminal->write("\n"); // Use $secret without logging it. } catch (\RuntimeException $e) { $terminal->write("\nUnable to read hidden input.\n"); }
readSecret(string $prompt = ''): string emits no characters, masks, backspace
sequences or newline. An explicit prompt goes to the session output; an empty
prompt writes nothing. Empty input returns ''; whitespace is preserved.
Backspace removes one encoded code point, not an entire grapheme cluster.
Ctrl+C, Ctrl+D and Escape cancel with \RuntimeException. Unavailable input,
write/read failures and failed mode restoration also raise \RuntimeException.
Invalid arguments and closed resources retain PHP's \TypeError / \ValueError
behavior. Exceptions from user-defined stream callbacks propagate unchanged.
Both reads restore the previous input mode before returning, including an outer raw mode. A failed restore is reported. On POSIX, pending PHP-buffered bytes are consumed first. Windows uses console key events: mixing PHP byte reads with native input is unsupported, and pending PHP-buffered bytes cause a read failure.
Keys and raw mode
readKey(?float $timeout = null, ?float $sequenceTimeout = null): Key|string|false
returns a named key, a printable encoded code point, or false on timeout or
unavailable input. Null timeout blocks; zero polls. On POSIX the default timeout
between escape/UTF-8 bytes is 25 ms; sequenceTimeout adjusts it. Windows uses
native events and ignores that argument.
Named keys are arrows, Enter, Tab, Backspace, Escape, Home, End, Delete, PageUp, PageDown, Resize and F1–F12. Shift+Tab maps to Tab. Other control bytes remain strings. Unknown POSIX escape sequences map to Escape. Modifier combinations and full grapheme clusters are not normalized.
For a prompt loop, keep one session alive:
$mode = $terminal->enableRawMode(); if ($mode === false) { throw new \RuntimeException('Cannot enable raw mode.'); } try { while (($key = $terminal->readKey()) !== false) { if ($key === Key::Enter || $key === Key::Escape || $key === "\x03") { break; } } } finally { if (!$terminal->restoreMode()) { throw new \RuntimeException('Cannot restore terminal mode.'); } }
Repeated enableRawMode() calls on one session return its active ModeToken.
restoreMode(?ModeToken $mode = null): bool restores the original mode and consumes
the token. Tokens cannot be cloned, serialized or reused after restoration.
The session retains its token, so unsetting only the caller's token variable does
not restore the session. Destruction of the session also attempts restoration,
even if the caller still holds the token.
Keep resource-backed input open until restored. Use one owner per terminal; independent sessions or other native code must coordinate changes and restore in reverse order. Cleanup runs during normal destruction and PHP shutdown, including uncaught exceptions, but cannot run after an uncatchable kill or runtime crash. Destructor cleanup cannot report restoration failure; use explicit restoration when the application must check it. Raw mode preserves output processing.
Output capabilities
| Method | Result |
|---|---|
isTty() |
Whether the output is a terminal |
supportsAnsi() / enableAnsi() |
Detect / enable ANSI or Windows VT support |
getSize() |
TerminalSize or false; positive COLUMNS and LINES are a fallback |
getColorDepth() |
ColorDepth::None, Standard, Extended or TrueColor |
supportsColor(ColorDepth $depth = ColorDepth::Standard) |
Whether that depth is available |
supportsTrueColor() |
Whether 24-bit color is available |
write(string $data) |
Bytes written, or false on failure; an unsuccessful write may be partial |
setTitle(string $title) / beep() |
Success as a boolean |
TerminalSize has readonly cols / rows and width / height aliases;
toArray() returns ['cols' => ..., 'rows' => ...].
Backend, Stream, ColorDepth and Key are unbacked enums: use ->name,
not ->value. ColorDepth::bits() returns 0, 4, 8 or 24.
Terminal::getBackend() returns Backend::Posix or Backend::Windows.
See the stub for every signature.
Upgrading to 1.0
This is the first stable API release, with intentional changes from pre-1.0:
- Keys and secrets now read the configured input rather than process stdin.
- Hidden input is silent. Render masks and newlines in the application if needed.
- Secret cancellation and operational failure throw
RuntimeException, replacingError. Update catches accordingly. fromStreams()requires an input argument. A live session cannot be reinitialized, and another Terminal object is not a stream argument.- Repeated raw-mode entry is idempotent. Explicit restore failures retain state so restoration can be retried while the resource remains valid.
The v0.9 session API, unbacked enums and dimensions object remain. Code from
0.8 or earlier must account for the session and enum changes too. The
Terminal\Terminal static facade and Terminal\* aliases remain available;
its getSize() returns an array and its hidden input uses the same 1.0 behavior.
No fixes are backported to pre-1.0 releases. See the changelog.
Build from source
Use the phpize and php-config belonging to the PHP binary that will load the
extension. Build in a path without spaces.
git clone https://github.com/prateekbhujel/php-terminal.git cd php-terminal git checkout v1.0.0 phpize ./configure --with-php-config="$(command -v php-config)" make -j2 make test REPORT_EXIT_STATUS=1 NO_INTERACTION=1 sudo make install
Enable extension=terminal.so in that runtime's php.ini, then run
php --ri terminal. On Apple Silicon with an x86_64 PHP distribution, configure
with CFLAGS="-arch x86_64" LDFLAGS="-arch x86_64" to match PHP's architecture.
Windows source builds use config.w32 and the PHP SDK; CI uses
php/php-windows-builder.
Run an example against a local build:
php -d extension=modules/terminal.so examples/doctor.php php -d extension=modules/terminal.so examples/prompt.php
Basic operations, stream output, diagnostics and a prompt loop use the session API. Read CONTRIBUTING.md for tests and bug reports and SECURITY.md for private security reports.