Skip to content

Add Io\Terminal native terminal primitives - #23941

Open
prateekbhujel wants to merge 52 commits into
php:masterfrom
prateekbhujel:rfc-io-terminal
Open

prateekbhujel wants to merge 52 commits into
php:masterfrom
prateekbhujel:rfc-io-terminal

Conversation

@prateekbhujel

@prateekbhujel prateekbhujel commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Implements the Io\Terminal RFC (v0.6) for PHP 8.7.

Adds Io\Terminal\Terminal with terminal dimensions (getDimensions() returning Dimensions), raw-mode leases (enableRawMode() / restoreMode() with ModeToken), and key, line, and hidden input. It works on POSIX terminals and Windows consoles, and also accepts ordinary streams so input can be scripted in tests.

Raw-mode leases are tracked per logical terminal. They can be released in any order or through another Terminal wrapping the same terminal, and the last release restores the saved mode. enableRawMode() is #[\NoDiscard] because a discarded token releases its lease immediately.

Tests are in ext/standard/tests/terminal/. They cover lease ownership and restoration (including PTYs and fork), key decoding, timeouts, resize, line and secret input, and handoff between reads on the same instance.

The RFC is still under discussion.

@prateekbhujel prateekbhujel changed the title Rfc io terminal Add Io\Terminal native terminal primitives Sep 27, 2026
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.stub.php Resolved
Comment thread ext/standard/io_poll.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved

@DanielEScherzer DanielEScherzer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some initial thoughts

Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.c Resolved
Comment thread ext/standard/io_terminal.stub.php Resolved
@NattyNarwhal

Copy link
Copy Markdown
Member

FWIW the timespec functions may be useful to extract into another utility file in the future. poll has php_poll_timespec_to_ms, if we use them in more places to deal with APIs like these.

Also, perhaps it might be useful to split the Unix/Windows backends into their own files, if it gets unwieldy.

@prateekbhujel

Copy link
Copy Markdown
Contributor Author

Yeah, both make sense. I kept them local for now since the terminal implementation is still fairly self-contained, but if the timespec helpers start being reused elsewhere, pulling them into a shared utility would be cleaner.

Same for the platform code, if the Unix/Windows sides keep growing, splitting them out would probably make the implementation easier to maintain.

Thanks for pointing it out.

…m handling

- Refactor public API to final Terminal and ModeToken classes under Nicolas's shared lease model
- Implement explicit mode restoration via restoreMode(ModeToken $mode): void with transactional rollback
- Implement bounded readiness and overall sequence deadlines for ordinary streams on POSIX and Windows
- Unify byte-stream readSecret cancellation (Escape, Ctrl+C, Ctrl+D) and SS3/CSI controls across platforms
- Harden CRLF lookahead across secret and line reading, preserving data CR before CRLF and at EOF
- Isolate test seam hooks to debug/test builds (ZEND_DEBUG || PHP_TERMINAL_TEST_BUILD)
- Add regression PHPT test suite covering fork isolation, failure injection, output flags, and streams
Drop the readLine() suppression and the EIO-to-EOF conversion from fe88929;
they were broader than the CI failure required and could hide real read errors.
Mark enableRawMode() with NoDiscard so accidental disposal of the lease is visible. Document token destruction, cover the warning and explicit discard, and remove two setup calls that discarded their tokens.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would drop the terminal_ from all test file names, that's redundant with the directory name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, dropped the terminal_ prefix across all test files in the directory.

--SKIPIF--
<?php
if (!class_exists(\Io\Terminal\Terminal::class)) die('skip Io\Terminal\Terminal not available');
if (!class_exists(\Time\Duration::class)) die('skip Time\Duration not available');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Time\Duration is guaranteed to be available. And my understanding is that the terminal class is as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the redundant class checks from the --SKIPIF-- sections.

Comment thread ext/standard/io_terminal.c Outdated
Comment on lines +104 to +123
#ifdef PHP_WIN32
typedef struct php_io_terminal_identity {
php_io_terminal_native_stream stream;
bool is_console;
bool is_non_tty_stream;
zend_ulong stream_res_id;
php_stream *php_stream_ptr;
} php_io_terminal_identity;
#else
typedef struct php_io_terminal_identity {
php_io_terminal_native_stream stream;
dev_t dev;
bool has_dev;
pid_t sid;
bool has_sid;
bool is_non_tty_stream;
zend_ulong stream_res_id;
php_stream *php_stream_ptr;
} php_io_terminal_identity;
#endif

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For readability I would suggest to only wrap the condition around the Unix/Windows differences and keep the common fields outside of the condition.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaned up: moved the common fields outside the #ifdef PHP_WIN32 block, and did the same for the other platform conditionals in the file.

Comment thread ext/standard/io_terminal.c Outdated
Comment on lines +183 to +187
#define PHP_IO_TERMINAL_READ_EOF (-2)
#define PHP_IO_TERMINAL_READ_ERROR (-1)
#define PHP_IO_TERMINAL_READ_TIMEOUT 0
#define PHP_IO_TERMINAL_READ_SUCCESS 1
#define PHP_IO_TERMINAL_READ_RESIZE 2

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this an enum.

@prateekbhujel prateekbhujel Nov 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C doesn't allow switching on function pointers (and SIG_DFL/SIG_IGN expand to pointer casts), but I simplified this to a boolean check (handler != SIG_DFL && handler != SIG_IGN).

Comment thread ext/standard/io_terminal.c Outdated
Comment on lines +217 to +226
if (handler == NULL) {
return false;
}
if (handler == SIG_DFL) {
return false;
}
if (handler == SIG_IGN) {
return false;
}
return true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switch() should be more readable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C doesn't allow switching on function pointers (and SIG_DFL/SIG_IGN expand to pointer casts), but I simplified this to a one-line boolean check (handler != NULL && handler != SIG_DFL && handler != SIG_IGN).

Comment thread ext/standard/io_terminal.c Outdated
unsigned char key;
int read_result = php_io_terminal_read_byte(fd, stream, poll_ctx, &key, timeout, true,
resize_handler_installed ? &resize_generation : NULL);
if (read_result == PHP_IO_TERMINAL_READ_RESIZE) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switch

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to switch (read_result).

Comment thread ext/standard/io_terminal.c Outdated
RETURN_THROWS();
}

zend_update_property_long(php_io_terminal_terminal_size_ce, Z_OBJ_P(ZEND_THIS), "cols", sizeof("cols") - 1, cols);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
zend_update_property_long(php_io_terminal_terminal_size_ce, Z_OBJ_P(ZEND_THIS), "cols", sizeof("cols") - 1, cols);
zend_update_property_long(php_io_terminal_terminal_size_ce, Z_OBJ_P(ZEND_THIS), "cols", strlen("cols"), cols);

In new code prefer strlen().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to strlen(), including the exception registration in MINIT.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants