Re: [RFC] Io\Terminal
| From: | Larry Garfield | Date: | Tue, 29 Sep 2026 14:35:07 +0000 |
| Subject: | Re: [RFC] Io\Terminal | ||
| References: | 1 2 3 4 5 6 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-132696@lists.php.net to get a copy of this message | ||
On Mon, Sep 28, 2026, at 11:13 PM, Pratik Bhujel wrote:
> On Mon, Sep 28, 2026, Larry Garfield wrote:
>> - As I'm not familiar with the underlying OS tools... what is raw mode?
>> That seems to be just glossed over. It looks like the only useful API
>> method (readKey() ) requires going into raw mode, so I wonder what its
>> purpose is.
>
> Fair point. The RFC was assuming too much terminal background there.
>
> In canonical mode the terminal normally buffers input until a line is
> complete, may echo it, and handles some control characters itself. Raw
> mode turns off that line-oriented processing so the application can
> react to individual key presses and terminal sequences directly.
>
> One thing that also wasn't clear is that callers don't need to call
> enableRawMode() before readKey() or readSecret(). Both handle the
> temporary mode change internally.
>
> enableRawMode() is there for longer-running interactive code that
> wants to keep the terminal raw across multiple reads/redraws.
>
> I've clarified that in the RFC.
Thanks, that does make it clearer! So the reason to use raw mode yourself would be, for instance,
for a game where you're capturing the arrow keys and WASD, or something like that? (Concrete
examples would help.).
This also makes me think that a readLine() method makes sense in this base tool, not at a higher
level. (I haven't done much console-GUI works so I am not familiar with the typical patterns,
but it seems like the natural complement to readKey().)
>> - That said, raw mode looks like a textbook case for a context manager. :-)
>
> Conceptually, yes. That's what I was trying to model with ModeToken:
> it represents the active lease, and restoreMode() gives an explicit
> way to release it in a try/finally.
>
> PHP doesn't have a general language-level context-manager construct,
> and I didn't want to add a terminal-specific callback abstraction just
> for this, so I kept the primitive explicit.
Yes, that's more of an aside for the audience. Arnaud and I have an RFC out (currently on
hold) for context managers, and this would be another very good use case for them.
>> - I understand all of the usual arguments for making the Terminal class
>> final. However, it also has no interface. That means it's basically
>> impossible to mock for testing purposes. That strikes me as a problem,
>> because any IO boundary should be mockable. I don't know that multiple
>> non-testing implementations makes sense (maybe alternatives to the
>> static constructors?), but we do need some straightforward mechanism to
>> mock a Terminal object. [...]
>
> I agree with the testing concern. I've added TerminalInterface while
> keeping the native Terminal final, so application and library code can
> type against something that can be replaced by a userland fake.
>
> I also added ModeTokenInterface for fake implementations. The native
> Terminal still only accepts a native token belonging to the same
> logical terminal; foreign, stale, or unrelated tokens are rejected
> with ValueError.
Conventions for Internals say to not use a *Interface suffix. It's unnecessary. I would
suggest either
interface Terminal {
public function readKey();
// ...
}
class SystemTerminal implements Terminal {
public static function fromStdIo(): self {}
public static function fromStreams(): self {}
}
or possibly:
class StdIoTerminal implements Terminal {
// .. No factories.
}
class StreamsTerminal implements Terminal {
public function __construct($in, $out = null) {}
}
For ModeToken, I'm not sure if it makes sense to have separate classes for each core terminal.
It's just an opaque value object, really, so I don't know what pattern we'd want
here.
--Larry Garfield