Hi internals, Tim, Nicolas and Larry, Sorry, I sent this update separately by mistake and left the list off Cc. Posting it here to keep the discussion in the RFC thread.
I've updated Io\Terminal to version 0.3 and pushed the corresponding implementation changes: https://wiki.php.net/rfc/io_terminal https://github.com/php/php-src/pull/23941 Thanks for the feedback. I revisited line input and naming after my last reply, and wanted to explain the decisions together here. 1. Naming and testability Terminal and ModeToken are now the interfaces, with final SystemTerminal and SystemModeToken as the native implementations. This follows the naming convention Larry pointed out and lets libraries use userland implementations for testing. I kept one native terminal class with fromStdio() and fromStreams(). Selecting the streams does not need a separate public class when the operations are otherwise the same. There is also one native token class. TerminalSize has a public constructor requiring positive dimensions, so tests can construct sizes directly. The RFC includes the actual stub and its strict-properties and non-serializable annotations. 2. Why readLine() is included My earlier reasoning focused too much on the overlap with fgets(). Having line input on Terminal gives libraries the same interface for keys, lines and hidden input, including native Windows console handling and coordination with raw-mode leases. readLine() removes LF/CRLF, preserves other whitespace, returns null on immediate EOF, and returns a final unterminated line at EOF. POSIX uses the existing line discipline; Windows uses native console line input and restores the previous mode. Redirected PHP streams retain their buffering, blocking and read-timeout behavior. It throws TerminalException while a managed raw-mode lease is active for that terminal, including one acquired through another wrapper. Silently overriding the mode would interfere with the code holding that lease. I kept the timeout parameter out because enforcing a portable whole-line deadline while preserving native editing would require more control over the editing loop. History, completion and richer editing remain userland concerns. 3. Raw-mode ownership and token lifetime The implementation uses the shared record model Nicolas suggested. The first lease saves the original mode and holds an independent restoration descriptor or handle. Later leases share that record, and the last release restores the saved mode, regardless of release order. Closing the original stream does not by itself prevent restoration. Wrappers for the same logical terminal coordinate, while unrelated PTYs remain separate. The token is opaque and can outlive its wrapper. SystemTerminal retains its latest token, so unsetting only the caller's variable may leave that lease active. Destruction and request shutdown attempt cleanup; explicit restoreMode() reports restoration failures. Native restoration rejects foreign, consumed or unrelated tokens with ValueError. Without an argument, restoreMode() returns false when the wrapper has no active retained token, including one already released through another wrapper. 4. When explicit raw mode is useful Games, arrow-key menus, search/select UIs and editors need input without waiting for Enter. An explicit lease keeps the terminal raw between reads and redraws. readKey() and readSecret() manage their own temporary mode, so individual reads do not require enableRawMode() first. Longer sessions use a lease with try/finally and restoreMode(). 5. Return values, timeouts and secret input getSize() returns null when no usable size is available. Key and secret reads return null on timeout. Operational failures, key-input EOF and secret cancellation throw TerminalException. readSecret() accepts an overall Time\Duration timeout. Echo is disabled during entry; the method does not display the typed characters. An empty submitted secret returns "", while a timeout returns null. Escape, Ctrl+C and Ctrl+D cancel with TerminalException. For timeout arguments, null means no finite deadline, zero makes a non-blocking attempt, and negative durations throw ValueError. readSecret() keeps escape-sequence handling internal. 6. Partial input and regression coverage The RFC now distinguishes incomplete UTF-8 bytes retained across key-read timeouts from incomplete POSIX escape sequences, which may return a consumed prefix. The POSIX sequence ambiguity window cannot extend the overall timeout. Regression tests cover cross-wrapper restoration and subsequent line reads, token lifetime, unrelated PTYs, and native POSIX line reads encountering EAGAIN/EWOULDBLOCK. Temporary unavailability now causes those native line reads to wait for input rather than being treated as EOF. I'd like to keep the feature set steady now and give version 0.3 the full 14-day cooldown. Once that has elapsed and substantive discussion is settled, I'll post a separate Intent to Vote. Implementation review and platform testing can continue alongside that. Best Regards, Pratik
