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

Reply via email to