Hi
On 2026-10-02 18:20, Larry Garfield wrote:
If I understand what you and Bob (thanks Bob) are suggesting, one would
do something like:
$in = fopen('php://memory');
$out = fopen('php://memory');
fputs($in, "first line\n");
fputs($in, "second line\n");
rewind($in);
$t = Terminal::fromStreams($in, $out);
$line = $t->readLine();
assert($line === 'first line');
$line = $t->readLine();
assert($line === 'second line');
Is that correct? Pratik, are you able to confirm (via tests) that this
would work as a testing approach, including for key and secret reads?
Except for the missing `$mode` on `fopen()`, *literally* just that would
work. I just compiled the branch and ran:
<?php
$in = fopen('php://memory', 'w+');
$out = fopen('php://memory', 'w+');
fputs($in, "first line\n");
fputs($in, "second line\n");
rewind($in);
$t = Io\Terminal\SystemTerminal::fromStreams($in, $out);
$line = $t->readLine();
var_dump($line);
$line = $t->readLine();
var_dump($line);
the output is:
string(10) "first line"
string(11) "second line"
However for `readKey()` et al you need a “PTY” handle, so you would, as
I mentioned in both of my replies, spawn sub-processes with stdin /
stdout being of type `pty` in the `descriptor_spec` (this incidentally
doesn't seem to be documented yet, except for the user comments) and
appropriate environment variables. Spawning the side using the
`Terminal` in the sub-process avoids weird inverted logic with regard to
which descriptor is the logical input and which one is the logical
output: Writing to the input handle emulates keyboard input and you can
assert on whatever you get back on the output handle:
<?php
use Io\Terminal\SystemTerminal;
function app(): void
{
$terminal = SystemTerminal::fromStdio();
// The mode is kept on the terminal, we only need the token
// for explicit resets.
(void)$terminal->enableRawMode();
// Synchronization point
echo "test ready\n";
$key = $terminal->readKey();
var_dump($key);
// Simulate timing / computation that happens.
sleep(1);
echo "Password?", PHP_EOL;
$secret = $terminal->readSecret();
echo "Got: ", $secret, "\n";
}
if (($argv[1] ?? null) === '--app') {
app();
exit();
}
$proc = proc_open(
[PHP_BINARY, __FILE__, '--app'],
[0 => ['pty'], 1 => ['pty'], 2 => ['pipe', 'w']],
$pipes,
null,
['TERM' => 'xterm-256color'],
);
// Wait for the synchronization point that confirms the application
is up and running
// and accepting keyboard input. In the real world this happens
through the user
// observing the output (which is slower than the application
start).
fgets($pipes[1]);
// And now press the “Up” arrow.
fwrite($pipes[0], "\e[A");
echo fgets($pipes[1]);
// Write the password in anticipation of the prompt (which is
delayed by computation,
// simulating user behavior).
fwrite($pipes[0], "hunter2\n");
// Assert the prompt and output confirming the password input.
$prompt = trim(fgets($pipes[1]));
assert($prompt === "Password?");
var_dump(trim(fgets($pipes[1])));
proc_close($proc);
The output of that is:
enum(Io\Terminal\Key::Up)
string(12) "Got: hunter2"
Spawning a process is comparatively heavy, but “testing the IO glue
code” is integration test territory, the business logic - e.g. the
actual password verification - is already tested independently as part
of a unit test.
If so, then I agree the interfaces become unnecessary, and the above
should be included in the RFC as the recommended testing approach.
Although, it occurs to me while typing the above, the Terminal accepts
an output stream, but doesn't appear to have any output API. When is
the output stream even used? Should it be removed, or a print() (or
similar) API added?
The output stream is used to determine the terminal size - which makes
sense, since you want to know the size of what you're writing to. For
output you can just `fwrite()` to the handle directly, but having some
output functionality makes sense to me as future scope (e.g. a function
to print some text with ANSI color escapes, which may be what's meant by
“ANSI support” in the future scope).
Best regards
Tim Düsterhus