Blame | Letzte Änderung | Log anzeigen | RSS feed
<?php/*** Hoa*** @license** New BSD License** Copyright © 2007-2017, Hoa community. All rights reserved.** Redistribution and use in source and binary forms, with or without* modification, are permitted provided that the following conditions are met:* * Redistributions of source code must retain the above copyright* notice, this list of conditions and the following disclaimer.* * Redistributions in binary form must reproduce the above copyright* notice, this list of conditions and the following disclaimer in the* documentation and/or other materials provided with the distribution.* * Neither the name of the Hoa nor the names of its contributors may be* used to endorse or promote products derived from this software without* specific prior written permission.** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE* POSSIBILITY OF SUCH DAMAGE.*/namespace Psy\Readline\Hoa;/*** Class \Hoa\Console\Readline.** Read, edit, bind… a line from the input.*/class Readline{/*** State: continue to read.*/const STATE_CONTINUE = 1;/*** State: stop to read.*/const STATE_BREAK = 2;/*** State: no output the current buffer.*/const STATE_NO_ECHO = 4;/*** Current editing line.*/protected $_line = null;/*** Current editing line seek.*/protected $_lineCurrent = 0;/*** Current editing line length.*/protected $_lineLength = 0;/*** Current buffer (most of the time, a char).*/protected $_buffer = null;/*** Mapping.*/protected $_mapping = [];/*** History.*/protected $_history = [];/*** History current position.*/protected $_historyCurrent = 0;/*** History size.*/protected $_historySize = 0;/*** Prefix.*/protected $_prefix = null;/*** Autocompleter.*/protected $_autocompleter = null;/*** Initialize the readline editor.*/public function __construct(){if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) {return;}$this->_mapping["\033[A"] = [$this, '_bindArrowUp'];$this->_mapping["\033[B"] = [$this, '_bindArrowDown'];$this->_mapping["\033[C"] = [$this, '_bindArrowRight'];$this->_mapping["\033[D"] = [$this, '_bindArrowLeft'];$this->_mapping["\001"] = [$this, '_bindControlA'];$this->_mapping["\002"] = [$this, '_bindControlB'];$this->_mapping["\005"] = [$this, '_bindControlE'];$this->_mapping["\006"] = [$this, '_bindControlF'];$this->_mapping["\010"] =$this->_mapping["\177"] = [$this, '_bindBackspace'];$this->_mapping["\027"] = [$this, '_bindControlW'];$this->_mapping["\n"] = [$this, '_bindNewline'];$this->_mapping["\t"] = [$this, '_bindTab'];return;}/*** Read a line from the input.*/public function readLine(string $prefix = null){$input = Console::getInput();if (true === $input->eof()) {return false;}$direct = Console::isDirect($input->getStream()->getStream());$output = Console::getOutput();if (false === $direct || \defined('PHP_WINDOWS_VERSION_PLATFORM')) {$out = $input->readLine();if (false === $out) {return false;}$out = \substr($out, 0, -1);if (true === $direct) {$output->writeAll($prefix);} else {$output->writeAll($prefix.$out."\n");}return $out;}$this->resetLine();$this->setPrefix($prefix);$read = [$input->getStream()->getStream()];$write = $except = [];$output->writeAll($prefix);while (true) {@\stream_select($read, $write, $except, 30, 0);if (empty($read)) {$read = [$input->getStream()->getStream()];continue;}$char = $this->_read();$this->_buffer = $char;$return = $this->_readLine($char);if (0 === ($return & self::STATE_NO_ECHO)) {$output->writeAll($this->_buffer);}if (0 !== ($return & self::STATE_BREAK)) {break;}}return $this->getLine();}/*** Readline core.*/public function _readLine(string $char){if (isset($this->_mapping[$char]) &&\is_callable($this->_mapping[$char])) {$mapping = $this->_mapping[$char];return $mapping($this);}if (isset($this->_mapping[$char])) {$this->_buffer = $this->_mapping[$char];} elseif (false === Ustring::isCharPrintable($char)) {ConsoleCursor::bip();return static::STATE_CONTINUE | static::STATE_NO_ECHO;}if ($this->getLineLength() === $this->getLineCurrent()) {$this->appendLine($this->_buffer);return static::STATE_CONTINUE;}$this->insertLine($this->_buffer);$tail = \mb_substr($this->getLine(),$this->getLineCurrent() - 1);$this->_buffer = "\033[K".$tail.\str_repeat("\033[D",\mb_strlen($tail) - 1);return static::STATE_CONTINUE;}/*** Add mappings.*/public function addMappings(array $mappings){foreach ($mappings as $key => $mapping) {$this->addMapping($key, $mapping);}}/*** Add a mapping.* Supported key:* • \e[… for \033[…;* • \C-… for Ctrl-…;* • abc for a simple mapping.* A mapping is a callable that has only one parameter of type* Hoa\Console\Readline and that returns a self::STATE_* constant.*/public function addMapping(string $key, $mapping){if ('\e[' === \substr($key, 0, 3)) {$this->_mapping["\033[".\substr($key, 3)] = $mapping;} elseif ('\C-' === \substr($key, 0, 3)) {$_key = \ord(\strtolower(\substr($key, 3))) - 96;$this->_mapping[\chr($_key)] = $mapping;} else {$this->_mapping[$key] = $mapping;}}/*** Add an entry in the history.*/public function addHistory(string $line = null){if (empty($line)) {return;}$this->_history[] = $line;$this->_historyCurrent = $this->_historySize++;}/*** Clear history.*/public function clearHistory(){unset($this->_history);$this->_history = [];$this->_historyCurrent = 0;$this->_historySize = 1;}/*** Get an entry in the history.*/public function getHistory(int $i = null){if (null === $i) {$i = $this->_historyCurrent;}if (!isset($this->_history[$i])) {return null;}return $this->_history[$i];}/*** Go backward in the history.*/public function previousHistory(){if (0 >= $this->_historyCurrent) {return $this->getHistory(0);}return $this->getHistory($this->_historyCurrent--);}/*** Go forward in the history.*/public function nextHistory(){if ($this->_historyCurrent + 1 >= $this->_historySize) {return $this->getLine();}return $this->getHistory(++$this->_historyCurrent);}/*** Get current line.*/public function getLine(){return $this->_line;}/*** Append to current line.*/public function appendLine(string $append){$this->_line .= $append;$this->_lineLength = \mb_strlen($this->_line);$this->_lineCurrent = $this->_lineLength;}/*** Insert into current line at the current seek.*/public function insertLine(string $insert){if ($this->_lineLength === $this->_lineCurrent) {return $this->appendLine($insert);}$this->_line = \mb_substr($this->_line, 0, $this->_lineCurrent).$insert.\mb_substr($this->_line, $this->_lineCurrent);$this->_lineLength = \mb_strlen($this->_line);$this->_lineCurrent += \mb_strlen($insert);return;}/*** Reset current line.*/protected function resetLine(){$this->_line = null;$this->_lineCurrent = 0;$this->_lineLength = 0;}/*** Get current line seek.*/public function getLineCurrent(): int{return $this->_lineCurrent;}/*** Get current line length.** @return int*/public function getLineLength(): int{return $this->_lineLength;}/*** Set prefix.*/public function setPrefix(string $prefix){$this->_prefix = $prefix;}/*** Get prefix.*/public function getPrefix(){return $this->_prefix;}/*** Get buffer. Not for user.*/public function getBuffer(){return $this->_buffer;}/*** Set an autocompleter.*/public function setAutocompleter(Autocompleter $autocompleter){$old = $this->_autocompleter;$this->_autocompleter = $autocompleter;return $old;}/*** Get the autocompleter.** @return ?Autocompleter*/public function getAutocompleter(){return $this->_autocompleter;}/*** Read on input. Not for user.*/public function _read(int $length = 512): string{return Console::getInput()->read($length);}/*** Set current line. Not for user.*/public function setLine(string $line){$this->_line = $line;$this->_lineLength = \mb_strlen($this->_line ?: '');$this->_lineCurrent = $this->_lineLength;}/*** Set current line seek. Not for user.*/public function setLineCurrent(int $current){$this->_lineCurrent = $current;}/*** Set line length. Not for user.*/public function setLineLength(int $length){$this->_lineLength = $length;}/*** Set buffer. Not for user.*/public function setBuffer(string $buffer){$this->_buffer = $buffer;}/*** Up arrow binding.* Go backward in the history.*/public function _bindArrowUp(self $self): int{if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) {ConsoleCursor::clear('↔');Console::getOutput()->writeAll($self->getPrefix());}$buffer = $self->previousHistory() ?? '';$self->setBuffer($buffer);$self->setLine($buffer);return static::STATE_CONTINUE;}/*** Down arrow binding.* Go forward in the history.*/public function _bindArrowDown(self $self): int{if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) {ConsoleCursor::clear('↔');Console::getOutput()->writeAll($self->getPrefix());}$self->setBuffer($buffer = $self->nextHistory());$self->setLine($buffer);return static::STATE_CONTINUE;}/*** Right arrow binding.* Move cursor to the right.*/public function _bindArrowRight(self $self): int{if ($self->getLineLength() > $self->getLineCurrent()) {if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) {ConsoleCursor::move('→');}$self->setLineCurrent($self->getLineCurrent() + 1);}$self->setBuffer('');return static::STATE_CONTINUE;}/*** Left arrow binding.* Move cursor to the left.*/public function _bindArrowLeft(self $self): int{if (0 < $self->getLineCurrent()) {if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) {ConsoleCursor::move('←');}$self->setLineCurrent($self->getLineCurrent() - 1);}$self->setBuffer('');return static::STATE_CONTINUE;}/*** Backspace and Control-H binding.* Delete the first character at the right of the cursor.*/public function _bindBackspace(self $self): int{$buffer = '';if (0 < $self->getLineCurrent()) {if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) {ConsoleCursor::move('←');ConsoleCursor::clear('→');}if ($self->getLineLength() === $current = $self->getLineCurrent()) {$self->setLine(\mb_substr($self->getLine(), 0, -1));} else {$line = $self->getLine();$current = $self->getLineCurrent();$tail = \mb_substr($line, $current);$buffer = $tail.\str_repeat("\033[D", \mb_strlen($tail));$self->setLine(\mb_substr($line, 0, $current - 1).$tail);$self->setLineCurrent($current - 1);}}$self->setBuffer($buffer);return static::STATE_CONTINUE;}/*** Control-A binding.* Move cursor to beginning of line.*/public function _bindControlA(self $self): int{for ($i = $self->getLineCurrent() - 1; 0 <= $i; --$i) {$self->_bindArrowLeft($self);}return static::STATE_CONTINUE;}/*** Control-B binding.* Move cursor backward one word.*/public function _bindControlB(self $self): int{$current = $self->getLineCurrent();if (0 === $current) {return static::STATE_CONTINUE;}$words = \preg_split('#\b#u',$self->getLine(),-1,\PREG_SPLIT_OFFSET_CAPTURE | \PREG_SPLIT_NO_EMPTY);for ($i = 0, $max = \count($words) - 1;$i < $max && $words[$i + 1][1] < $current;++$i) {}for ($j = $words[$i][1] + 1; $current >= $j; ++$j) {$self->_bindArrowLeft($self);}return static::STATE_CONTINUE;}/*** Control-E binding.* Move cursor to end of line.*/public function _bindControlE(self $self): int{for ($i = $self->getLineCurrent(), $max = $self->getLineLength();$i < $max;++$i) {$self->_bindArrowRight($self);}return static::STATE_CONTINUE;}/*** Control-F binding.* Move cursor forward one word.*/public function _bindControlF(self $self): int{$current = $self->getLineCurrent();if ($self->getLineLength() === $current) {return static::STATE_CONTINUE;}$words = \preg_split('#\b#u',$self->getLine(),-1,\PREG_SPLIT_OFFSET_CAPTURE | \PREG_SPLIT_NO_EMPTY);for ($i = 0, $max = \count($words) - 1;$i < $max && $words[$i][1] < $current;++$i) {}if (!isset($words[$i + 1])) {$words[$i + 1] = [1 => $self->getLineLength()];}for ($j = $words[$i + 1][1]; $j > $current; --$j) {$self->_bindArrowRight($self);}return static::STATE_CONTINUE;}/*** Control-W binding.* Delete first backward word.*/public function _bindControlW(self $self): int{$current = $self->getLineCurrent();if (0 === $current) {return static::STATE_CONTINUE;}$words = \preg_split('#\b#u',$self->getLine(),-1,\PREG_SPLIT_OFFSET_CAPTURE | \PREG_SPLIT_NO_EMPTY);for ($i = 0, $max = \count($words) - 1;$i < $max && $words[$i + 1][1] < $current;++$i) {}for ($j = $words[$i][1] + 1; $current >= $j; ++$j) {$self->_bindBackspace($self);}return static::STATE_CONTINUE;}/*** Newline binding.*/public function _bindNewline(self $self): int{$self->addHistory($self->getLine());return static::STATE_BREAK;}/*** Tab binding.*/public function _bindTab(self $self): int{$output = Console::getOutput();$autocompleter = $self->getAutocompleter();$state = static::STATE_CONTINUE | static::STATE_NO_ECHO;if (null === $autocompleter) {return $state;}$current = $self->getLineCurrent();$line = $self->getLine();if (0 === $current) {return $state;}$matches = \preg_match_all('#'.$autocompleter->getWordDefinition().'$#u',\mb_substr($line, 0, $current),$words);if (0 === $matches) {return $state;}$word = $words[0][0];if ('' === \trim($word)) {return $state;}$solution = $autocompleter->complete($word);$length = \mb_strlen($word);if (null === $solution) {return $state;}if (\is_array($solution)) {$_solution = $solution;$count = \count($_solution) - 1;$cWidth = 0;$window = ConsoleWindow::getSize();$wWidth = $window['x'];$cursor = ConsoleCursor::getPosition();\array_walk($_solution, function (&$value) use (&$cWidth) {$handle = \mb_strlen($value);if ($handle > $cWidth) {$cWidth = $handle;}return;});\array_walk($_solution, function (&$value) use (&$cWidth) {$handle = \mb_strlen($value);if ($handle >= $cWidth) {return;}$value .= \str_repeat(' ', $cWidth - $handle);return;});$mColumns = (int) \floor($wWidth / ($cWidth + 2));$mLines = (int) \ceil(($count + 1) / $mColumns);--$mColumns;$i = 0;if (0 > $window['y'] - $cursor['y'] - $mLines) {ConsoleWindow::scroll('↑', $mLines);ConsoleCursor::move('↑', $mLines);}ConsoleCursor::save();ConsoleCursor::hide();ConsoleCursor::move('↓ LEFT');ConsoleCursor::clear('↓');foreach ($_solution as $j => $s) {$output->writeAll("\033[0m".$s."\033[0m");if ($i++ < $mColumns) {$output->writeAll(' ');} else {$i = 0;if (isset($_solution[$j + 1])) {$output->writeAll("\n");}}}ConsoleCursor::restore();ConsoleCursor::show();++$mColumns;$input = Console::getInput();$read = [$input->getStream()->getStream()];$write = $except = [];$mColumn = -1;$mLine = -1;$coord = -1;$unselect = function () use (&$mColumn,&$mLine,&$coord,&$_solution,&$cWidth,$output) {ConsoleCursor::save();ConsoleCursor::hide();ConsoleCursor::move('↓ LEFT');ConsoleCursor::move('→', $mColumn * ($cWidth + 2));ConsoleCursor::move('↓', $mLine);$output->writeAll("\033[0m".$_solution[$coord]."\033[0m");ConsoleCursor::restore();ConsoleCursor::show();return;};$select = function () use (&$mColumn,&$mLine,&$coord,&$_solution,&$cWidth,$output) {ConsoleCursor::save();ConsoleCursor::hide();ConsoleCursor::move('↓ LEFT');ConsoleCursor::move('→', $mColumn * ($cWidth + 2));ConsoleCursor::move('↓', $mLine);$output->writeAll("\033[7m".$_solution[$coord]."\033[0m");ConsoleCursor::restore();ConsoleCursor::show();return;};$init = function () use (&$mColumn,&$mLine,&$coord,&$select) {$mColumn = 0;$mLine = 0;$coord = 0;$select();return;};while (true) {@\stream_select($read, $write, $except, 30, 0);if (empty($read)) {$read = [$input->getStream()->getStream()];continue;}switch ($char = $self->_read()) {case "\033[A":if (-1 === $mColumn && -1 === $mLine) {$init();break;}$unselect();$coord = \max(0, $coord - $mColumns);$mLine = (int) \floor($coord / $mColumns);$mColumn = $coord % $mColumns;$select();break;case "\033[B":if (-1 === $mColumn && -1 === $mLine) {$init();break;}$unselect();$coord = \min($count, $coord + $mColumns);$mLine = (int) \floor($coord / $mColumns);$mColumn = $coord % $mColumns;$select();break;case "\t":case "\033[C":if (-1 === $mColumn && -1 === $mLine) {$init();break;}$unselect();$coord = \min($count, $coord + 1);$mLine = (int) \floor($coord / $mColumns);$mColumn = $coord % $mColumns;$select();break;case "\033[D":if (-1 === $mColumn && -1 === $mLine) {$init();break;}$unselect();$coord = \max(0, $coord - 1);$mLine = (int) \floor($coord / $mColumns);$mColumn = $coord % $mColumns;$select();break;case "\n":if (-1 !== $mColumn && -1 !== $mLine) {$tail = \mb_substr($line, $current);$current -= $length;$self->setLine(\mb_substr($line, 0, $current).$solution[$coord].$tail);$self->setLineCurrent($current + \mb_strlen($solution[$coord]));ConsoleCursor::move('←', $length);$output->writeAll($solution[$coord]);ConsoleCursor::clear('→');$output->writeAll($tail);ConsoleCursor::move('←', \mb_strlen($tail));}// no breakdefault:$mColumn = -1;$mLine = -1;$coord = -1;ConsoleCursor::save();ConsoleCursor::move('↓ LEFT');ConsoleCursor::clear('↓');ConsoleCursor::restore();if ("\033" !== $char && "\n" !== $char) {$self->setBuffer($char);return $self->_readLine($char);}break 2;}}return $state;}$tail = \mb_substr($line, $current);$current -= $length;$self->setLine(\mb_substr($line, 0, $current).$solution.$tail);$self->setLineCurrent($current + \mb_strlen($solution));ConsoleCursor::move('←', $length);$output->writeAll($solution);ConsoleCursor::clear('→');$output->writeAll($tail);ConsoleCursor::move('←', \mb_strlen($tail));return $state;}}/** Advanced interaction.*/Console::advancedInteraction();