PHP RFC: Io\Terminal
- Version: 0.1
- Date: 2026-09-27
- Author: Pratik Bhujel, prateekbhujelpb@gmail.com
- Status: Draft
- Target Version: PHP 8.7
- Implementation: https://github.com/php/php-src/pull/23941
- Discussion thread: tbd
- Voting thread: tbd
Introduction
PHP does not currently provide a cross-platform API for basic terminal operations such as querying the terminal size, switching the terminal into raw mode, reading individual keys, or reading secret input without echo.
Userland libraries generally implement these features using platform-specific workarounds. On POSIX systems this often involves invoking tools such as stty, while Windows requires the Win32 Console API.
This RFC proposes a small native terminal API in the Io\Terminal namespace.
<?php use Io\Terminal\Key; use Io\Terminal\Terminal; use Time\Duration; $terminal = Terminal::create(); $size = $terminal->getSize(); if ($size !== false) { printf("%dx%d\n", $size->cols, $size->rows); } $mode = $terminal->enableRawMode(); try { $key = $terminal->readKey(Duration::fromSeconds(5)); if ($key === Key::Escape) { echo "Escape pressed\n"; } } finally { if ($mode !== false) { $terminal->restoreMode($mode); } }
Proposal
The RFC adds an object-oriented terminal API under Io\Terminal.
The proposed API is:
<?php namespace Io\Terminal; class TerminalException extends \Io\IoException {} enum Key { case Up; case Down; case Right; case Left; case Enter; case Backspace; case Escape; case Tab; case Home; case End; case Delete; case PageUp; case PageDown; case Resize; case F1; case F2; case F3; case F4; case F5; case F6; case F7; case F8; case F9; case F10; case F11; case F12; } final readonly class TerminalSize { public readonly int $cols; public readonly int $rows; private function __construct() {} } final class ModeToken { private function __construct() {} } final class Terminal { private function __construct() {} public static function create(): Terminal {} /** * @param resource $input * @param resource|null $output */ public static function fromStreams($input, $output = null): Terminal {} public function getSize(): TerminalSize|false {} public function enableRawMode(): ModeToken|false {} public function restoreMode(?ModeToken $mode = null): bool {} public function readKey( ?\Time\Duration $timeout = null, ?\Time\Duration $sequenceTimeout = null, ): Key|string|false {} public function readSecret(): string {} }
Terminal and ModeToken are not serializable.
Creating a terminal session
Terminal::create() creates a terminal session using the process standard input and standard output.
Terminal::fromStreams() creates a terminal session using an explicit input stream and an optional output stream. If the output stream is omitted, the input stream is also used as the output stream.
The constructor is private. Terminal instances are created using these named constructors.
Terminal size
Terminal::getSize() queries the size of the terminal associated with the output side of the session.
On success it returns a TerminalSize object containing the number of columns and rows. It returns false if a native terminal size cannot be obtained.
No fallback to the COLUMNS or LINES environment variables is performed by this API.
Raw mode
Terminal::enableRawMode() enables raw input mode for the session input and returns a ModeToken. It returns false if raw mode cannot be enabled.
Terminal::restoreMode() restores a saved terminal mode. A token can be passed explicitly, or the method can be called without an argument to restore the active mode owned by the Terminal instance.
ModeToken is an opaque object. An active token restores its saved terminal mode when it is destroyed. A Terminal instance also restores an active mode it owns when the object is destroyed. Active modes are restored during normal request shutdown as well.
These restoration mechanisms are intended to avoid leaving a terminal in raw or no-echo mode after ordinary PHP control flow exits. They cannot provide a guarantee for process termination that bypasses PHP shutdown entirely.
Reading keys
Terminal::readKey() reads one logical key from the session input.
Recognized special keys are returned as Io\Terminal\Key enum cases. Printable input is returned as a string. Input that does not map to a known enum case is also returned as a string rather than discarded.
Key::Resize is returned when a terminal resize event is observed.
The optional $timeout controls how long the method waits for input. A null timeout waits indefinitely. A zero duration performs a non-blocking poll.
On POSIX, $sequenceTimeout controls the short wait used while reading multi-byte input and terminal escape sequences. If it is omitted, the implementation uses a 25 ms sequence timeout. Windows receives structured console input events and does not use this value.
Both timeout parameters use Time\Duration. Negative durations result in a ValueError.
Reading secret input
Terminal::readSecret() reads input without echoing the entered characters.
Enter completes the input. Backspace removes the previous character. Escape, Ctrl+C, and Ctrl+D cancel the read.
If the read cannot be completed, or if the terminal mode cannot be restored, the method throws Io\Terminal\TerminalException.
TerminalException extends Io\IoException.
Motivation
Terminal handling is inherently platform-specific. POSIX systems expose terminal functionality through APIs such as termios and ioctl, while Windows uses the Win32 Console API.
Without native support, PHP libraries need external processes, FFI, or other platform-specific workarounds for these operations.
The API proposed here is based on experience from the ext-terminal extension and from integrating that extension with Symfony Console/TUI. The core proposal is intentionally smaller than the complete ext-terminal API.
Backward Incompatible Changes
No existing PHP API is changed.
The RFC introduces the following names:
Io\Terminal\TerminalIo\Terminal\TerminalSizeIo\Terminal\ModeTokenIo\Terminal\TerminalExceptionIo\Terminal\Key
Userland code declaring one of these exact names would conflict with the new internal symbol.
Proposed PHP Version(s)
PHP 8.7.
RFC Impact
To the Ecosystem
CLI libraries may use the native API when it is available and retain their existing fallbacks for older PHP versions.
The ext-terminal extension can continue to serve as a backport and as a place to experiment with functionality that is outside this RFC.
To Existing Extensions
The implementation is part of ext/standard.
No existing extension API is removed or changed.
To SAPIs
The main use case is CLI and other environments attached to a terminal.
The API itself is available independently of SAPI. Operations that require a terminal return false or throw TerminalException as described above when the associated stream is not a usable terminal.
Open Issues
None currently.
Future Scope
The following functionality exists in the reference extension or has been explored separately, but is not proposed by this RFC:
- lower-level terminal input events
- ANSI support detection
- terminal color-depth detection
- setting the terminal title
- emitting a terminal bell
- environment-variable based terminal-size fallbacks
These can be considered separately if there is sufficient need and the API semantics are clear.
Voting Choices
The primary vote will be:
Implement the Io\Terminal API as described in this RFC?
The available choices will be Yes, No, and Abstain.
Acceptance requires a 2/3 majority of Yes votes over No votes.
Patches and Tests
Implementation:
https://github.com/php/php-src/pull/23941
Reference extension:
https://github.com/prateekbhujel/php-terminal
The implementation includes PHPT coverage for terminal construction, terminal sizing, raw mode and restoration, key input, escape sequences, timeout validation, and secret input.
Implementation
Not implemented.
References
Previous discussion:
Related work:
Rejected Features
Earlier iterations considered a larger API. The following are not part of this RFC:
- exposing the same operations through both procedural functions and object methods
- separate stdin, stdout, and stderr terminal objects instead of a single terminal session
- a public constructor in addition to named constructors
- separate
getWidth()andgetHeight()methods in addition togetSize() - compatibility-only APIs from older ext-terminal releases
- the complete ext-terminal 1.x API
Changelog
- 2026-09-27: Initial draft.