createStdin($loop); // @codeCoverageIgnore
}
if ($output === null) {
$output = $this->createStdout($loop); // @codeCoverageIgnore
}
if ($readline === null) {
$readline = new Readline($input, $output, $this);
}
$this->input = $input;
$this->output = $output;
$this->readline = $readline;
$that = $this;
// readline data emits a new line
$incomplete =& $this->incompleteLine;
$this->readline->on('data', function($line) use ($that, &$incomplete) {
// readline emits a new line on enter, so start with a blank line
$incomplete = '';
$that->emit('data', array($line));
});
// handle all input events (readline forwards all input events)
$this->readline->on('error', array($this, 'handleError'));
$this->readline->on('end', array($this, 'handleEnd'));
$this->readline->on('close', array($this, 'handleCloseInput'));
// handle all output events
$this->output->on('error', array($this, 'handleError'));
$this->output->on('close', array($this, 'handleCloseOutput'));
}
public function __destruct()
{
$this->restoreTtyMode();
}
public function pause()
{
$this->input->pause();
}
public function resume()
{
$this->input->resume();
}
public function isReadable()
{
return $this->input->isReadable();
}
public function isWritable()
{
return $this->output->isWritable();
}
public function pipe(WritableStreamInterface $dest, array $options = array())
{
Util::pipe($this, $dest, $options);
return $dest;
}
public function write($data)
{
// return false if already ended, return true if writing empty string
if ($this->ending || $data === '') {
return !$this->ending;
}
$out = $data;
$lastNewline = strrpos($data, "\n");
$restoreReadline = false;
if ($this->incompleteLine !== '') {
// the last write did not end with a newline => append to existing row
// move one line up and move cursor to last position before writing data
$out = "\033[A" . "\r\033[" . $this->width($this->incompleteLine) . "C" . $out;
// data contains a newline, so this will overwrite the readline prompt
if ($lastNewline !== false) {
// move cursor to beginning of readline prompt and clear line
// clearing is important because $data may not overwrite the whole line
$out = "\r\033[K" . $out;
// make sure to restore readline after this output
$restoreReadline = true;
}
} else {
// here, we're writing to a new line => overwrite readline prompt
// move cursor to beginning of readline prompt and clear line
$out = "\r\033[K" . $out;
// we always overwrite the readline prompt, so restore it on next line
$restoreReadline = true;
}
// following write will have have to append to this line if it does not end with a newline
$endsWithNewline = substr($data, -1) === "\n";
if ($endsWithNewline) {
// line ends with newline, so this is line is considered complete
$this->incompleteLine = '';
} else {
// always end data with newline in order to append readline on next line
$out .= "\n";
if ($lastNewline === false) {
// contains no newline at all, everything is incomplete
$this->incompleteLine .= $data;
} else {
// contains a newline, everything behind it is incomplete
$this->incompleteLine = (string)substr($data, $lastNewline + 1);
}
}
if ($restoreReadline) {
// write output and restore original readline prompt and line buffer
return $this->output->write($out . $this->readline->getDrawString());
} else {
// restore original cursor position in readline prompt
$pos = $this->width($this->readline->getPrompt()) + $this->readline->getCursorCell();
if ($pos !== 0) {
// we always start at beginning of line, move right by X
$out .= "\033[" . $pos . "C";
}
// write to actual output stream
return $this->output->write($out);
}
}
public function end($data = null)
{
if ($this->ending) {
return;
}
if ($data !== null) {
$this->write($data);
}
$this->ending = true;
// clear readline output, close input and end output
$this->readline->setInput('')->setPrompt('');
$this->input->close();
$this->output->end();
}
public function close()
{
if ($this->closed) {
return;
}
$this->ending = true;
$this->closed = true;
$this->input->close();
$this->output->close();
$this->emit('close');
$this->removeAllListeners();
}
/**
* @deprecated
* @return Readline
*/
public function getReadline()
{
return $this->readline;
}
/**
* prompt to prepend to input line
*
* Will redraw the current input prompt with the current input buffer.
*
* @param string $prompt
* @return void
*/
public function setPrompt($prompt)
{
$this->readline->setPrompt($prompt);
}
/**
* returns the prompt to prepend to input line
*
* @return string
* @see self::setPrompt()
*/
public function getPrompt()
{
return $this->readline->getPrompt();
}
/**
* sets whether/how to echo text input
*
* The default setting is `true`, which means that every character will be
* echo'ed as-is, i.e. you can see what you're typing.
* For example: Typing "test" shows "test".
*
* You can turn this off by supplying `false`, which means that *nothing*
* will be echo'ed while you're typing. This could be a good idea for
* password prompts. Note that this could be confusing for users, so using
* a character replacement as following is often preferred.
* For example: Typing "test" shows "" (nothing).
*
* Alternative, you can supply a single character replacement character
* that will be echo'ed for each character in the text input. This could
* be a good idea for password prompts, where an asterisk character ("*")
* is often used to indicate typing activity and password length.
* For example: Typing "test" shows "****" (with asterisk replacement)
*
* Changing this setting will redraw the current prompt and echo the current
* input buffer according to the new setting.
*
* @param boolean|string $echo echo can be turned on (boolean true) or off (boolean true), or you can supply a single character replacement string
* @return void
*/
public function setEcho($echo)
{
$this->readline->setEcho($echo);
}
/**
* whether or not to support moving cursor left and right
*
* switching cursor support moves the cursor to the end of the current
* input buffer (if any).
*
* @param boolean $move
* @return void
*/
public function setMove($move)
{
$this->readline->setMove($move);
}
/**
* Gets current cursor position measured in number of text characters.
*
* Note that the number of text characters doesn't necessarily reflect the
* number of monospace cells occupied by the text characters. If you want
* to know the latter, use `self::getCursorCell()` instead.
*
* @return int
* @see self::getCursorCell() to get the position measured in monospace cells
* @see self::moveCursorTo() to move the cursor to a given character position
* @see self::moveCursorBy() to move the cursor by given number of characters
* @see self::setMove() to toggle whether the user can move the cursor position
*/
public function getCursorPosition()
{
return $this->readline->getCursorPosition();
}
/**
* Gets current cursor position measured in monospace cells.
*
* Note that the cell position doesn't necessarily reflect the number of
* text characters. If you want to know the latter, use
* `self::getCursorPosition()` instead.
*
* Most "normal" characters occupy a single monospace cell, i.e. the ASCII
* sequence for "A" requires a single cell, as do most UTF-8 sequences
* like "Ä".
*
* However, there are a number of code points that do not require a cell
* (i.e. invisible surrogates) or require two cells (e.g. some asian glyphs).
*
* Also note that this takes the echo mode into account, i.e. the cursor is
* always at position zero if echo is off. If using a custom echo character
* (like asterisk), it will take its width into account instead of the actual
* input characters.
*
* @return int
* @see self::getCursorPosition() to get current cursor position measured in characters
* @see self::moveCursorTo() to move the cursor to a given character position
* @see self::moveCursorBy() to move the cursor by given number of characters
* @see self::setMove() to toggle whether the user can move the cursor position
* @see self::setEcho()
*/
public function getCursorCell()
{
return $this->readline->getCursorCell();
}
/**
* Moves cursor to right by $n chars (or left if $n is negative).
*
* Zero value or values out of range (exceeding current input buffer) are
* simply ignored.
*
* Will redraw() the readline only if the visible cell position changes,
* see `self::getCursorCell()` for more details.
*
* @param int $n
* @return void
*/
public function moveCursorBy($n)
{
$this->readline->moveCursorBy($n);
}
/**
* Moves cursor to given position in current line buffer.
*
* Values out of range (exceeding current input buffer) are simply ignored.
*
* Will redraw() the readline only if the visible cell position changes,
* see `self::getCursorCell()` for more details.
*
* @param int $n
* @return void
*/
public function moveCursorTo($n)
{
$this->readline->moveCursorTo($n);
}
/**
* Appends the given input to the current text input buffer at the current position
*
* This moves the cursor accordingly to the number of characters added.
*
* @param string $input
* @return void
*/
public function addInput($input)
{
$this->readline->addInput($input);
}
/**
* set current text input buffer
*
* this moves the cursor to the end of the current
* input buffer (if any).
*
* @param string $input
* @return void
*/
public function setInput($input)
{
$this->readline->setInput($input);
}
/**
* get current text input buffer
*
* @return string
*/
public function getInput()
{
return $this->readline->getInput();
}
/**
* Adds a new line to the (bottom position of the) history list
*
* @param string $line
* @return void
*/
public function addHistory($line)
{
$this->readline->addHistory($line);
}
/**
* Clears the complete history list
*
* @return void
*/
public function clearHistory()
{
$this->readline->clearHistory();
}
/**
* Returns an array with all lines in the history
*
* @return string[]
*/
public function listHistory()
{
return $this->readline->listHistory();
}
/**
* Limits the history to a maximum of N entries and truncates the current history list accordingly
*
* @param int|null $limit
* @return void
*/
public function limitHistory($limit)
{
$this->readline->limitHistory($limit);
}
/**
* set autocompletion handler to use
*
* The autocomplete handler will be called whenever the user hits the TAB
* key.
*
* @param callable|null $autocomplete
* @return void
* @throws \InvalidArgumentException if the given callable is invalid
*/
public function setAutocomplete($autocomplete)
{
$this->readline->setAutocomplete($autocomplete);
}
/**
* whether or not to emit a audible/visible BELL signal when using a disabled function
*
* By default, this class will emit a BELL signal when using a disable function,
* such as using the left or backspace keys when
* already at the beginning of the line.
*
* Whether or not the BELL is audible/visible depends on the termin and its
* settings, i.e. some terminals may "beep" or flash the screen or emit a
* short vibration.
*
* @param bool $bell
* @return void
*/
public function setBell($bell)
{
$this->readline->setBell($bell);
}
private function width($str)
{
return $this->readline->strwidth($str) - 2 * substr_count($str, "\x08");
}
/** @internal */
public function handleError(\Exception $e)
{
$this->emit('error', array($e));
$this->close();
}
/** @internal */
public function handleEnd()
{
$this->emit('end');
}
/** @internal */
public function handleCloseInput()
{
$this->restoreTtyMode();
if (!$this->output->isWritable()) {
$this->close();
}
}
/** @internal */
public function handleCloseOutput()
{
if (!$this->input->isReadable()) {
$this->close();
}
}
/**
* @codeCoverageIgnore this is covered by functional tests with/without ext-readline
*/
private function restoreTtyMode()
{
if (function_exists('readline_callback_handler_remove')) {
// remove dummy readline handler to turn to default input mode
readline_callback_handler_remove();
} elseif ($this->originalTtyMode !== null && is_resource(STDIN) && $this->isTty()) {
// Reset stty so it behaves normally again
shell_exec('stty ' . escapeshellarg($this->originalTtyMode));
$this->originalTtyMode = null;
}
// restore blocking mode so following programs behave normally
if (defined('STDIN') && is_resource(STDIN)) {
stream_set_blocking(STDIN, true);
}
}
/**
* @param ?LoopInterface $loop
* @return ReadableStreamInterface
* @codeCoverageIgnore this is covered by functional tests with/without ext-readline
*/
private function createStdin(LoopInterface $loop = null)
{
// STDIN not defined ("php -a") or already closed (`fclose(STDIN)`)
// also support starting program with closed STDIN ("example.php 0<&-")
// the stream is a valid resource and is not EOF, but fstat fails
if (!defined('STDIN') || !is_resource(STDIN) || fstat(STDIN) === false) {
$stream = new ReadableResourceStream(fopen('php://memory', 'r'), $loop);
$stream->close();
return $stream;
}
$stream = new ReadableResourceStream(STDIN, $loop);
if (function_exists('readline_callback_handler_install')) {
// Prefer `ext-readline` to install dummy handler to turn on raw input mode.
// We will never actually feed the readline handler and instead
// handle all input in our `Readline` implementation.
readline_callback_handler_install('', function () { });
return $stream;
}
if ($this->isTty()) {
$this->originalTtyMode = rtrim(shell_exec('stty -g'), PHP_EOL);
// Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
shell_exec('stty -icanon -echo');
}
// register shutdown function to restore TTY mode in case of unclean shutdown (uncaught exception)
// this will not trigger on SIGKILL etc., but the terminal should take care of this
register_shutdown_function(array($this, 'close'));
return $stream;
}
/**
* @param ?LoopInterface $loop
* @return WritableStreamInterface
* @codeCoverageIgnore this is covered by functional tests
*/
private function createStdout(LoopInterface $loop = null)
{
// STDOUT not defined ("php -a") or already closed (`fclose(STDOUT)`)
// also support starting program with closed STDOUT ("example.php >&-")
// the stream is a valid resource and is not EOF, but fstat fails
if (!defined('STDOUT') || !is_resource(STDOUT) || fstat(STDOUT) === false) {
$output = new WritableResourceStream(fopen('php://memory', 'r+'), $loop);
$output->close();
} else {
$output = new WritableResourceStream(STDOUT, $loop);
}
return $output;
}
/**
* @return bool
* @codeCoverageIgnore
*/
private function isTty()
{
if (PHP_VERSION_ID >= 70200) {
// Prefer `stream_isatty()` (available as of PHP 7.2 only)
return stream_isatty(STDIN);
} elseif (function_exists('posix_isatty')) {
// Otherwise use `posix_isatty` if available (requires `ext-posix`)
return posix_isatty(STDIN);
}
// otherwise try to guess based on stat file mode and device major number
// Must be special character device: ($mode & S_IFMT) === S_IFCHR
// And device major number must be allocated to TTYs (2-5 and 128-143)
// For what it's worth, checking for device gid 5 (tty) is less reliable.
// @link http://man7.org/linux/man-pages/man7/inode.7.html
// @link https://www.kernel.org/doc/html/v4.11/admin-guide/devices.html#terminal-devices
$stat = fstat(STDIN);
$mode = isset($stat['mode']) ? ($stat['mode'] & 0170000) : 0;
$major = isset($stat['dev']) ? (($stat['dev'] >> 8) & 0xff) : 0;
return ($mode === 0020000 && $major >= 2 && $major <= 143 && ($major <=5 || $major >= 128));
}
}