Re: [RFC] Io\Terminal

From: 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

« previous php.internals (#132696) next »