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

Reply via email to