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

Reply via email to