public function listen() { Cursor::setStyle('▋', true); do { Cursor::colorize('b fg(yellow) bg(blue)'); echo '◼ datatext > '; Cursor::colorize('!b fg(default) bg(default)'); $line = $this->readline->readLine(' '); if ($line === 'fake') { $rows = []; $size = Window::getSize(); for ($i = 0; $i < $size['y'] - 4; $i++) { $row = []; for ($j = 0; $j < 10; $j++) { $row[] = $this->faker->name; } $rows[] = $row; } $table = new Table($rows, ['headers' => ['a', 'b', 'c', 'd', 'e'], 'stickyColumns' => [2]]); $navigable = new NavigableTable($table); $navigable->render(); } else { echo '< ', $line, "\n"; } } while (false !== $line && 'quit' !== $line); }
function __construct($options = []) { if (array_key_exists('columnOffset', $options)) { if (!is_integer($options['columnOffset'])) { throw new \InvalidArgumentException('Invalid column offset. Not an integer'); } $this->columnOffset = $options['columnOffset']; } $this->screenEstate = Window::getSize(); if (array_key_exists('screenEstate', $options)) { foreach (['x', 'y'] as $dimension) { if (array_key_exists($dimension, $options['screenEstate'])) { if (!is_integer($options['screenEstate'][$dimension])) { throw new \InvalidArgumentException('Invalid screen estate dimension. Not an integer'); } $this->screenEstate[$dimension] = $options['screenEstate'][$dimension]; } } } }
/** * Main method. * * @return int */ function main() { $operation = 0; $location = null; $updateServer = Updater::DEFAULT_UPDATE_SERVER; while (false !== ($c = $this->getOption($v))) { switch ($c) { case '__ambiguous': $this->resolveOptionAmbiguity($v); break; case 'f': $operation = static::OPERATION_FETCH | Updater::FORMAT_PHAR; break; case 'z': $operation = static::OPERATION_FETCH | Updater::FORMAT_ZIP; break; case 'a': $operation = static::OPERATION_APPLY; $location = $v; break; case 's': $updateServer = $v; break; case 'h': case '?': default: return $this->usage(); break; } } $updateServer = rtrim($updateServer, '/') . '/'; if (0 !== (static::OPERATION_FETCH & $operation)) { $updatesDotJson = Updater::getUpdateUrl($updateServer); $versions = @file_get_contents($updatesDotJson); if (empty($versions)) { throw new Exception\Console('Oh no! We are not able to check if a new version exists… ' . 'Contact us at http://sabre.io/ ' . '(tried URL %s).', 0, $updatesDotJson); } $versions = json_decode($versions, true); /** * Expected format: * { * "1.0.1": { * "phar": "https://…", * "zip" : "https://…" * }, * "1.0.0": { * "phar": "https://…", * "zip" : "https://…" * }, * … * } */ $versionsToFetch = Updater::filterVersions($versions, Version::VERSION, $operation); $windowWidth = Window::getSize()['x']; $progress = function ($percent) use($windowWidth) { Cursor::clear('↔'); $message = 'Downloading… '; $barWidth = $windowWidth - mb_strlen($message); if ($percent <= 0) { $color = '#c74844'; } elseif ($percent <= 25) { $color = '#cb9a3d'; } elseif ($percent <= 50) { $color = '#dcb11e'; } elseif ($percent <= 75) { $color = '#aed633'; } else { $color = '#54b455'; } echo $message; Cursor::colorize('foreground(' . $color . ') background(' . $color . ')'); echo str_repeat('|', $percent * $barWidth / 100); Cursor::colorize('normal'); }; foreach ($versionsToFetch as $versionNumber => $versionUrl) { list(, $versionUrlBasename) = Uri\split($versionUrl); $fileIn = new File\Read($versionUrl, File::MODE_READ, null, true); $fileOut = new File\Write(SABRE_KATANA_PREFIX . '/data/share/update/' . $versionUrlBasename); echo "\n", 'Fetch version ', $versionNumber, ' from ', $versionUrl, "\n", 'Waiting…', "\n"; $fileIn->on('connect', function () { Cursor::clear('↔'); echo 'Downloading… '; }); $fileIn->on('progress', function (Core\Event\Bucket $bucket) use($progress) { static $previousPercent = 0; $data = $bucket->getData(); $current = $data['transferred']; $max = $data['max']; $percent = $current * 100 / $max; $delta = $percent - $previousPercent; if (1 <= $delta) { $previousPercent = $percent; $progress($percent); } }); $fileIn->open(); $fileOut->writeAll($fileIn->readAll()); echo "\n", 'Fetched at ', $fileOut->getStreamName(), '.', "\n"; } return 0; } elseif (static::OPERATION_APPLY === $operation) { if (false === file_exists($location)) { throw new Exception\Console('Update %s is not found.', 1, $location); } $processus = new Console\Processus(Core::getPHPBinary(), [$location, '--extract' => SABRE_KATANA_PREFIX, '--overwrite']); $processus->on('input', function () { return false; }); $processus->on('output', function (Core\Event\Bucket $bucket) { echo $bucket->getData()['line'], "\n"; }); $processus->run(); if (true === $processus->isSuccessful()) { echo 'sabre/katana updated!', "\n"; } else { echo 'Something wrong happened!', "\n"; } return $processus->getExitCode(); } else { return $this->usage(); } }
/** * Main method. * * @return int */ function main() { $verbose = !(Console::isDirect(STDOUT) || !OS_WIN); while (false !== ($c = $this->getOption($v))) { switch ($c) { case '__ambiguous': $this->resolveOptionAmbiguity($v); break; case 'v': $verbose = $v; break; case 'h': case '?': default: return $this->usage(); } } if (true === Installer::isInstalled()) { echo 'The application is already installed.', "\n"; return 1; } $oldTitle = Window::getTitle(); Window::setTitle('Installation of sabre/katana'); $form = ['baseUrl' => '/', 'email' => null, 'password' => null, 'database' => ['driver' => 'sqlite', 'host' => '', 'port' => '', 'name' => '', 'username' => '', 'password' => '']]; $readline = new Console\Readline(); if (true === $verbose) { $windowWidth = Window::getSize()['x']; $labelMaxWidth = 35; $inputMaxWidth = $windowWidth - $labelMaxWidth; $numberOfSteps = 5; $input = function ($default = '') use($inputMaxWidth) { return Text::colorize($default . str_repeat(' ', $inputMaxWidth - mb_strlen($default)), 'foreground(black) background(#cccccc)'); }; $resetInput = function ($default = '') use($input, $labelMaxWidth) { Cursor::move('→', $labelMaxWidth); echo $input($default); Cursor::move('LEFT'); Cursor::move('→', $labelMaxWidth); Cursor::colorize('foreground(black) background(#cccccc)'); }; echo Text::colorize('Installation of sabre/' . "\n" . Welcome::LOGO, 'foreground(yellow)'), "\n\n", static::getBaseURLInfo(), "\n\n", 'Choose the base URL: ', $input('/'), "\n", 'Your administrator login: '******'Choose the administrator password: '******'Choose the administrator email: ', $input(), "\n", 'Choose the database driver: ', '🔘 SQLite ⚪️ MySQL', "\n"; Window::scroll('↑', 10); Cursor::move('↑', 10); Cursor::move('↑', $numberOfSteps); Cursor::move('→', $labelMaxWidth); // Disable arrow up and down. $no_echo = function ($readline) { return $readline::STATE_NO_ECHO; }; $readline->addMapping("[A", $no_echo); $readline->addMapping("[B", $no_echo); $step = function ($index, $label, callable $validator, $errorMessage, $default = '') use($numberOfSteps, &$readline, $resetInput, $labelMaxWidth) { Cursor::colorize('foreground(black) background(#cccccc)'); do { $out = $readline->readLine(); if (empty($out)) { $out = $default; } $valid = $validator($out); if (true !== $valid) { Cursor::move('↑'); $resetInput($default); Cursor::save(); Cursor::move('LEFT'); Cursor::move('↓', $numberOfSteps - $index + 1); list($title, $message) = explode("\n", $errorMessage); Cursor::colorize('foreground(white) background(red)'); echo $title, "\n"; Cursor::colorize('foreground(red) background(normal)'); echo $message; Cursor::restore(); } else { Cursor::save(); Cursor::move('LEFT'); Cursor::move('↓', $numberOfSteps - $index - 1); Cursor::colorize('normal'); Cursor::clear('↓'); Cursor::restore(); } } while (true !== $valid); if ($numberOfSteps !== $index + 1) { Cursor::move('→', $labelMaxWidth); } Cursor::colorize('normal'); return $out; }; $progress = function ($percent, $message) use($windowWidth) { static $margin = 4; $barWidth = $windowWidth - $margin * 2; Cursor::move('LEFT'); Cursor::move('↑', 1); Cursor::clear('↓'); if ($percent <= 0) { $color = '#c74844'; } elseif ($percent <= 25) { $color = '#cb9a3d'; } elseif ($percent <= 50) { $color = '#dcb11e'; } elseif ($percent <= 75) { $color = '#aed633'; } else { $color = '#54b455'; } echo str_repeat(' ', $margin); Cursor::colorize('foreground(' . $color . ') background(' . $color . ')'); echo str_repeat('|', $percent * $barWidth / 100); Cursor::move('LEFT ↓'); Cursor::colorize('background(normal)'); echo str_repeat(' ', $margin) . $message; Cursor::colorize('normal'); sleep(1); }; } else { echo 'Installation of sabre/' . "\n" . Welcome::LOGO, "\n\n", static::getBaseURLInfo(), "\n\n"; $step = function ($index, $label, callable $validator, $errorMessage, $default = '') use(&$readline) { do { echo $label; if (!empty($default)) { echo ' [default: ', $default, ']'; } $out = $readline->readLine(': '); if (empty($out)) { $out = $default; } $valid = $validator($out); if (true !== $valid) { echo $errorMessage, "\n"; } } while (true !== $valid); return $out; }; $progress = function ($percent, $message) { echo $message, "\n"; }; } $form['baseUrl'] = $step(0, 'Choose the base URL', function ($baseUrl) use($verbose) { $valid = Installer::checkBaseUrl($baseUrl); if (true === $valid && true === $verbose) { Cursor::move('↓'); } return $valid; }, 'Base URL must start and end by a slash' . "\n" . 'Check the Section “The base URL” on http://sabre.io/dav/gettingstarted/.', '/'); if (false === $verbose) { echo 'Your administrator login: '******'password'] = $step(1, 'Choose the administrator password', function ($administratorPassword) { return Installer::checkPassword($administratorPassword . $administratorPassword); }, 'Password must not be empty' . "\n" . 'An empty password is not a password anymore!'); $readline = $oldReadline; $form['email'] = $step(2, 'Choose the administrator email', function ($administratorEmail) { return Installer::checkEmail($administratorEmail . $administratorEmail); }, 'Email is invalid' . "\n" . 'The given email seems invalid.'); $databaseDriver =& $form['database']['driver']; if (true === $verbose) { $radioReadline = new Console\Readline\Password(); $radioReadline->addMapping('\\e[D', function () use($labelMaxWidth, &$databaseDriver) { $databaseDriver = 'sqlite'; Cursor::save(); Cursor::move('LEFT'); Cursor::move('→', $labelMaxWidth); Cursor::clear('→'); echo '🔘 SQLite ⚪️ MySQL'; Cursor::restore(); }); $radioReadline->addMapping('\\e[C', function () use($labelMaxWidth, &$databaseDriver) { $databaseDriver = 'mysql'; Cursor::save(); Cursor::move('LEFT'); Cursor::move('→', $labelMaxWidth); Cursor::clear('→'); echo '⚪️ SQLite 🔘 MySQL'; Cursor::restore(); }); Cursor::hide(); $radioReadline->readLine(); Cursor::show(); unset($databaseDriver); if ('mysql' === $form['database']['driver']) { echo 'Choose MySQL host: ', $input(), "\n", 'Choose MySQL port: ', $input('3306'), "\n", 'Choose MySQL username: '******'Choose MySQL password: '******'Choose MySQL database name: ', $input(), "\n"; Window::scroll('↑', 10); Cursor::move('↑', 10); $numberOfSteps = 5; Cursor::move('↑', $numberOfSteps); Cursor::move('→', $labelMaxWidth); Cursor::colorize('foreground(black) background(#cccccc)'); } } else { $form['database']['driver'] = $step(3, 'Choose the database driver (sqlite or mysql)', function ($databaseDriver) { return in_array($databaseDriver, ['sqlite', 'mysql']); }, 'Database driver is invalid' . "\n" . 'Database driver must be `sqlite` or `mysql`', 'sqlite'); } if ('mysql' === $form['database']['driver']) { $form['database']['host'] = $step(0, 'Choose MySQL host', function () { return true; }, ''); $form['database']['port'] = $step(1, 'Choose MySQL port', function ($port) { return false !== filter_var($port, FILTER_VALIDATE_INT); }, 'Port is invalid' . "\n" . 'Port must be an integer.', '3306'); $form['database']['username'] = $step(2, 'Choose MySQL username', function () { return true; }, ''); $oldReadline = $readline; $readline = new Console\Readline\Password(); $form['database']['password'] = $step(3, 'Choose MySQL password', function () { return true; }, ''); $readline = $oldReadline; $form['database']['name'] = $step(3, 'Choose MySQL database name', function () { return true; }, ''); } $readline->readLine("\n" . 'Ready to install? (Enter to continue, Ctrl-C to abort)'); echo "\n\n"; try { $progress(5, 'Create configuration file…'); $configuration = Installer::createConfigurationFile(Server::CONFIGURATION_FILE, ['baseUrl' => $form['baseUrl'], 'database' => $form['database']]); $progress(25, 'Configuration file created 👍!'); $progress(30, 'Create the database…'); $database = Installer::createDatabase($configuration); $progress(50, 'Database created 👍!'); $progress(55, 'Create administrator profile…'); Installer::createAdministratorProfile($configuration, $database, $form['email'], $form['password']); $progress(75, 'Administrator profile created 👍!'); $progress(100, 'sabre/katana is ready!'); } catch (\Exception $e) { $progress(-1, 'An error occured: ' . $e->getMessage()); if (null !== ($previous = $e->getPrevious())) { echo 'Underlying error: ' . $previous->getMessage(); } echo "\n", 'You are probably likely to run: ' . '`make uninstall` before trying again.', "\n"; return 2; } list($dirname) = Uri\split($form['baseUrl']); echo "\n\n", 'The administration interface will be found at this path: ', '<your website>', $dirname, '/admin.php.', "\n"; Window::setTitle($oldTitle); }
/** * Make a render of an operation. * * @accesss public * @param string $text The operation text. * @param bool $status The operation status. * @return void */ public function status($text, $status) { $window = \Hoa\Console\Window::getSize(); $out = ' ' . \Hoa\Console\Chrome\Text::colorize('*', 'foreground(yellow)') . ' ' . $text . str_pad(' ', $window['x'] - strlen(preg_replace('#' . "" . '\\[[0-9]+m#', '', $text)) - 8) . ($status === true ? '[' . \Hoa\Console\Chrome\Text::colorize('ok', 'foreground(green)') . ']' : '[' . \Hoa\Console\Chrome\Text::colorize('!!', 'foreground(white) background(red)') . ']'); echo $out, "\n"; return; }
* @return void */ public static function copy($data) { if (OS_WIN) { return; } echo "]52;;" . base64_encode($data) . "\\"; return; } } } namespace { /** * Advanced interaction. */ \Hoa\Console::advancedInteraction(); /** * Event. */ if (function_exists('pcntl_signal')) { \Hoa\Console\Window::getInstance(); pcntl_signal(SIGWINCH, function () { static $_window = null; if (null === $_window) { $_window = \Hoa\Console\Window::getInstance(); } \Hoa\Core\Event::notify('hoa://Event/Console/Window:resize', $_window, new \Hoa\Core\Event\Bucket(array('size' => \Hoa\Console\Window::getSize()))); }); } }
/** * Tab binding. * * @access public * @param \Hoa\Console\Readline $self Self. * @return int */ public function _bindTab(Readline $self) { $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', $line, $words, PREG_OFFSET_CAPTURE); if (0 === $matches) { return $state; } for ($i = 0, $max = count($words[0]); $i < $max && $current > $words[0][$i][1]; ++$i) { } $word = $words[0][$i - 1]; if ('' === trim($word[0])) { return $state; } $prefix = mb_substr($word[0], 0, $current - $word[1]); $solution = $autocompleter->complete($prefix); $length = mb_strlen($prefix); if (null === $solution) { return $state; } if (is_array($solution)) { $_solution = $solution; $count = count($_solution) - 1; $cWidth = 0; $window = \Hoa\Console\Window::getSize(); $wWidth = $window['x']; $cursor = \Hoa\Console\Cursor::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) { \Hoa\Console\Window::scroll('↑', $mLines); \Hoa\Console\Cursor::move('↑', $mLines); } \Hoa\Console\Cursor::save(); \Hoa\Console\Cursor::hide(); \Hoa\Console\Cursor::move('↓ LEFT'); \Hoa\Console\Cursor::clear('↓'); foreach ($_solution as $j => $s) { echo "[0m", $s, "[0m"; if ($i++ < $mColumns) { echo ' '; } else { $i = 0; if (isset($_solution[$j + 1])) { echo "\n"; } } } \Hoa\Console\Cursor::restore(); \Hoa\Console\Cursor::show(); ++$mColumns; $read = array(STDIN); $mColumn = -1; $mLine = -1; $coord = -1; $unselect = function () use(&$mColumn, &$mLine, &$coord, &$_solution, &$cWidth) { \Hoa\Console\Cursor::save(); \Hoa\Console\Cursor::hide(); \Hoa\Console\Cursor::move('↓ LEFT'); \Hoa\Console\Cursor::move('→', $mColumn * ($cWidth + 2)); \Hoa\Console\Cursor::move('↓', $mLine); echo "[0m" . $_solution[$coord] . "[0m"; \Hoa\Console\Cursor::restore(); \Hoa\Console\Cursor::show(); return; }; $select = function () use(&$mColumn, &$mLine, &$coord, &$_solution, &$cWidth) { \Hoa\Console\Cursor::save(); \Hoa\Console\Cursor::hide(); \Hoa\Console\Cursor::move('↓ LEFT'); \Hoa\Console\Cursor::move('→', $mColumn * ($cWidth + 2)); \Hoa\Console\Cursor::move('↓', $mLine); echo "[7m" . $_solution[$coord] . "[0m"; \Hoa\Console\Cursor::restore(); \Hoa\Console\Cursor::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 = array(STDIN); continue; } switch ($char = $self->_read()) { case "[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 "[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 "[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 "[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])); \Hoa\Console\Cursor::move('←', $length); echo $solution[$coord]; \Hoa\Console\Cursor::clear('→'); echo $tail; \Hoa\Console\Cursor::move('←', mb_strlen($tail)); } default: $mColumn = -1; $mLine = -1; $coord = -1; \Hoa\Console\Cursor::save(); \Hoa\Console\Cursor::move('↓ LEFT'); \Hoa\Console\Cursor::clear('↓'); \Hoa\Console\Cursor::restore(); if ("" !== $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)); \Hoa\Console\Cursor::move('←', $length); echo $solution; \Hoa\Console\Cursor::clear('→'); echo $tail; \Hoa\Console\Cursor::move('←', mb_strlen($tail)); return $state; }
/** * My own wordwrap (just force the wordwrap() $cut parameter).. * * @access public * @param string $text Text to wrap. * @param int $width Line width. * @param string $break String to make the break. * @return string */ public static function wordwrap($text, $width = null, $break = "\n") { if (null === $width) { $window = \Hoa\Console\Window::getSize(); $width = $window['x']; } return wordwrap($text, $width, $break, true); }
* Set clipboard value. * * @param string $data Data to copy. * @return void */ public static function copy($data) { if (OS_WIN) { return; } echo "]52;;" . base64_encode($data) . "\\"; return; } } /** * Advanced interaction. */ Console::advancedInteraction(); /** * Event. */ if (function_exists('pcntl_signal')) { Window::getInstance(); pcntl_signal(SIGWINCH, function () { static $_window = null; if (null === $_window) { $_window = Window::getInstance(); } Core\Event::notify('hoa://Event/Console/Window:resize', $_window, new Core\Event\Bucket(['size' => \Window::getSize()])); }); }
/** * Tab binding. * * @access public * @param Readline $self Self. * @return int */ public function _bindTab(Readline $self) { $autocompleter = $self->getAutocompleter(); $state = static::STATE_CONTINUE | static::STATE_NO_ECHO; if (null === $autocompleter) { return $state; } $current = $self->getLineCurrent(); $line = $self->getLine(); // we need at least 1 char to work with // and if it's the start of a line, we'll echo it. if (0 === $current || trim($line) === '') { $this->appendLine("\t"); $this->_buffer .= "\t"; return static::STATE_CONTINUE; } $matches = preg_match_all('#' . $autocompleter->getWordDefinition() . '#u', $line, $words, PREG_OFFSET_CAPTURE); if (0 === $matches) { return $state; } for ($i = 0, $max = count($words[1]); $i < $max && $current > $words[1][$i][1]; ++$i) { } $word = $words[1][$i - 1]; if ('' === trim($word[0])) { return $state; } $prefix = mb_substr($word[0], 0, $current - $word[1]); //Debug::log('prefix', compact('line','current','words','word','prefix')); $solution = $autocompleter->complete($prefix); #Debug::log('solution', $solution); if (null === $solution || empty($solution)) { return $state; } if (preg_match('/[\\S]+([\\W]+)([\\S]*)/', $prefix, $m, PREG_OFFSET_CAPTURE)) { #Debug::log('matchPrefix', compact('prefix', 'm')); $suffixLength = mb_strlen($m[2][0]); $prefix = mb_substr($prefix, -$suffixLength); $tail = mb_substr($line, $current); $head = mb_substr($line, 0, $current - $suffixLength); $line = $head . $tail; $current -= $suffixLength; $length = $suffixLength; } else { $length = mb_strlen($prefix); $tail = mb_substr($line, $current); $head = mb_substr($line, 0, $current - $length); $line = $head . $tail; $current -= $length; } #Debug::log('completionSetup', compact('prefix','line','current','head','tail','length')); if (is_array($solution)) { if (count($solution) === 1) { $line = $head . $solution[0] . $tail; $self->setLine($line); $self->setLineCurrent($current + mb_strlen($solution[0])); $self->setBuffer($line); Cursor::move('left', $length); echo $solution[0]; Cursor::clear('right'); echo $tail; Cursor::move('left', mb_strlen($tail)); #Debug::log('completion', array( # 'line' =>$self->getLine(), # 'current' =>$self->getLineCurrent(), # 'buffer' =>$self->getBuffer(), #)); return $state; } $_solution = $solution; $window = Window::getSize(); $cursor = Cursor::getPosition(); $wWidth = $window['x']; while (1) { $count = count($_solution) - 1; $cWidth = 0; array_walk($_solution, function ($value) use(&$cWidth) { $handle = mb_strlen($value); if ($handle > $cWidth) { $cWidth = $handle; } }); array_walk($_solution, function (&$value) use($cWidth) { $handle = mb_strlen($value); if ($handle < $cWidth) { $value .= str_repeat(' ', $cWidth - $handle); } }); $mColumns = (int) floor($wWidth / ($cWidth + 2)); $mLines = (int) ceil(($count + 1) / $mColumns); --$mColumns; if ($mLines >= $window['y']) { $toRemove = ($mLines - $window['y']) * $mColumns + 1; for ($i = 0; $i < $toRemove; $i++) { array_pop($_solution); } } else { break; } } $pos = Cursor::getPosition(); if ($window['y'] - $cursor['y'] - $mLines < 0) { echo str_repeat("\n", $mLines + 1); Cursor::move('up', $mLines + 1); Cursor::clear('LEFT'); echo $this->getPrefix() . $this->getLine() . "\n"; Cursor::move('up LEFT'); Cursor::move('right', $pos['x'] - 1); } Cursor::save(); Cursor::hide(); Cursor::move('down LEFT'); Cursor::clear('down'); $i = 0; foreach ($_solution as $j => $s) { echo "[0m", $s, "[0m"; if ($i++ < $mColumns) { echo ' '; } else { $i = 0; if (isset($_solution[$j + 1])) { echo "\n"; } } } Cursor::restore(); Cursor::show(); ++$mColumns; $read = array(STDIN); $mColumn = -1; $mLine = -1; $coord = -1; $unselect = function () use(&$mColumn, &$mLine, &$coord, &$_solution, &$cWidth) { Cursor::save(); Cursor::hide(); Cursor::move('down LEFT'); Cursor::move('right', $mColumn * ($cWidth + 2)); Cursor::move('down', $mLine); echo "[0m" . $_solution[$coord] . "[0m"; Cursor::restore(); Cursor::show(); }; $select = function () use(&$mColumn, &$mLine, &$coord, &$_solution, &$cWidth) { Cursor::save(); Cursor::hide(); Cursor::move('down LEFT'); Cursor::move('right', $mColumn * ($cWidth + 2)); Cursor::move('down', $mLine); echo "[7m" . $_solution[$coord] . "[0m"; Cursor::restore(); Cursor::show(); }; $init = function () use(&$mColumn, &$mLine, &$coord, &$select) { $mColumn = 0; $mLine = 0; $coord = 0; $select(); }; while (true) { @stream_select($read, $write, $except, 30, 0); if (empty($read)) { $read = array(STDIN); continue; } switch ($char = $self->_read()) { case "[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 "[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 "[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 "[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) { $self->setLine($head . $solution[$coord] . $tail); $self->setLineCurrent($current + mb_strlen($solution[$coord])); Cursor::move('left', $length); echo $solution[$coord]; Cursor::clear('right'); echo $tail; Cursor::move('left', mb_strlen($tail)); } default: $mColumn = -1; $mLine = -1; $coord = -1; Cursor::save(); Cursor::move('down LEFT'); Cursor::clear('down'); Cursor::restore(); if ("" !== $char && "\n" !== $char) { $self->setBuffer($char); return $self->_readLine($char); } break 2; } } return $state; } }