PHP CLI programs often need to read one key, hide a password, or ask how large the terminal is. Today, libraries reach for stty or the Windows Console API. Pratik Bhujel's Io\Terminal RFC adds one native API for those jobs on POSIX systems and Windows.
How it works
You create a terminal from standard input and output, or provide your own streams:
use Io\Terminal\Key; use Io\Terminal\Terminal; use Time\Duration; $terminal = Terminal::fromStdio(); $dimensions = $terminal->getDimensions(); if ($dimensions !== null) { printf("%d columns, %d rows\n", $dimensions->cols, $dimensions->rows); } $key = $terminal->readKey(Duration::fromSeconds(5)); if ($key === Key::Escape) { echo "Escape pressed\n"; }
getDimensions() returns a Dimensions value or null when no usable size exists. readKey() returns a Key enum case for keys such as arrows, Enter, Escape, Insert, Shift+Tab, and F1 through F12. Other input comes back as a string, while a timeout returns null. The API also includes readLine() and readSecret(), which reads a password without echoing it.
Holding raw mode
The key and secret methods manage temporary terminal modes. A menu that redraws after every key can hold raw mode itself:
$mode = $terminal->enableRawMode(); try { // interactive loop } finally { $terminal->restoreMode($mode); }
Raw-mode leases can be shared by multiple parts of an application. The original mode returns when the last lease is released. You have to pass the token back to restoreMode(), because the terminal doesn't remember the latest one for you. Dropping the last reference to a token also releases it, so enableRawMode() is marked #[\NoDiscard]: if you throw the token away, PHP warns you, since raw mode would end right away.
Testing without a terminal
Terminal and ModeToken are final classes, not interfaces. To test prompts, you hand Terminal::fromStreams() an ordinary stream with scripted input:
$input = fopen('php://temp', 'w+'); fwrite($input, "Widget\nsecret\n"); rewind($input); $terminal = Terminal::fromStreams($input); assert($terminal->readLine() === 'Widget'); assert($terminal->readSecret() === 'secret');
On an ordinary stream, enableRawMode() returns a lease that does nothing instead of failing. Streams can't fake a terminal size or a resize, so code that needs those still needs a native test or its own abstraction.
Where it stands
Bhujel opened the internals discussion on September 27, 2026. Version 0.4, published October 6, replaced the public interfaces with final classes and made the restoreMode() token required. The php-src implementation is under review, and the RFC targets PHP 8.7. Bhujel had planned to open the vote on October 23, but held off after Tim Düsterhus asked him to wait for the discussion to settle. On November 6 he announced a vote for November 12, then held off again the same day. Tim Düsterhus asked for examples of how leases behave, and Frode Børli, who switched his phasync/tui library to the new API, suggested cutting the RFC down to raw mode and terminal size and leaving key decoding to libraries. Version 0.5, published that day, adds the lease examples and the #[\NoDiscard] attribute but keeps the read methods. Version 0.6, on November 9, renamed TerminalSize and getSize() to Dimensions and getDimensions() at Tim's suggestion, added a $sequenceTimeout argument to readSecret(), and spells out that readKey() has no modifiers, mouse or paste support, so libraries with their own decoder can use just raw mode and getDimensions(). Bhujel is keeping the read methods for small scripts, and says he'll propose a new vote date once the discussion settles. Acceptance requires a two-thirds majority. The change adds new names under Io\Terminal; existing PHP APIs stay unchanged. Code or extensions declaring the same names will conflict, and libraries still need fallbacks for older PHP versions.