Re: [RFC] Io\Terminal
| From: | Frode Børli | Date: | Fri, 06 Nov 2026 10:15:17 +0000 |
| Subject: | Re: [RFC] Io\Terminal | ||
| References: | 1 2 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-133022@lists.php.net to get a copy of this message | ||
Hi Pratik, hi internals,
I maintain phasync/tui, a terminal UI library for PHP (layouts, widgets, and a VT emulator so shells
and full-screen programs run in panes). I built php-src#23941 and switched tui's terminal
handling to Io\Terminal where it exists:
https://github.com/phasync/tui/commit/eb7f919
Raw mode is now a ModeToken lease and the size comes from getSize(), instead of four stty runs (stty
-g, stty raw -echo, stty <saved>, stty size). tui's test suite gives the same results on
the PR build as on stock PHP, including the tests that crash a program in a pty in various ways and
check with stty -a that the terminal was restored. That part of the RFC works, and it is exactly
what userland cannot do without spawning stty, using FFI, or Windows-specific tricks. Thank you for
it.
I would like to suggest that the RFC proceeds with a reduced scope: the operations userland now
shells out for, and nothing that userland already does well.
What userland needs from core today:
* Saving the mode, entering raw mode, restoring it: Laravel Prompts runs stty -g and stty
<mode> (src/Terminal.php), Symfony Console runs stty in Terminal and in QuestionHelper for
hidden input, tui ran stty -g / raw -echo / restore.
* The size: Symfony Console uses stty -a and mode CON, tui used stty size.
enableRawMode()/restoreMode(), getSize(), TerminalSize, ModeToken and TerminalException cover all of
these, on POSIX and Windows.
What userland already does well, so it need not be in core:
* Key decoding. php-tui/term has a 600-line EventParser with modifiers, mouse and focus events;
Laravel Prompts keeps its own key table, modified arrows included; tui's InputDecoder reads
legacy xterm keys with modifiers, the kitty keyboard protocol, modifyOtherKeys, SGR and X10 mouse,
bracketed paste and focus reports. readKey() returns Key|string|null with 28 keys and no modifiers,
so none of these libraries could switch to it, and the enum cannot gain modifiers later without a
new method beside it.
* readLine() overlaps with fgets(); readSecret() is policy (Escape, Ctrl+C and Ctrl+D cancel,
Backspace edits, no masking) that is a few lines in userland once raw mode exists, and that prompt
libraries will want to decide for themselves.
If key input is wanted in core later, I think a separate RFC could do it as a pure decoder (bytes
in, events out, plus a flush for a lone Escape), with an event object that has room for modifiers,
mouse and paste. It would serve blocking scripts and event loops alike, and avoid decoder state that
belongs to one Terminal instance.
Smaller points on the reduced scope:
1. Factories: fromStreams() is enough. It already covers STDIN/STDOUT, a pty, and /dev/tty
(CONIN$/CONOUT$) when stdio is redirected, which is common for prompts in piped scripts. fromStdio()
re-resolves the standard handles on each call; is there a use case that fromStreams(STDIN, STDOUT)
does not cover? If not, one factory is easier to explain.
2. Raw mode on Windows: libraries that read and write escape sequences need virtual terminal
input (ENABLE_VIRTUAL_TERMINAL_INPUT) and output processing. The RFC leaves the console flags to the
implementation. Could raw mode specify them, or take an option for them?
3. Ctrl+C: some programs want raw input but Ctrl+C still delivered as a signal. An optional
argument to enableRawMode() would allow that now or in a later version without breaking anything.
4. Resize: with readKey() out, Key::Resize goes too. On POSIX a cheap getSize() plus SIGWINCH is
enough; on Windows the console reports resizes as input records, which a later input RFC could
cover.
I would support Io\Terminal in that form, and I am happy to test further revisions with tui.