/** * Main method. * * @return int */ function main() { $prefix = SABRE_KATANA_PREFIX; $verbose = Console::isDirect(STDOUT); while (false !== ($c = $this->getOption($v))) { switch ($c) { case 'p': echo $prefix, "\n"; return 0; break; case 'v': echo SABRE_KATANA_VERSION, "\n"; return 0; break; case 'V': $verbose = !$v; break; case '__ambiguous': $this->resolveOptionAmbiguity($v); break; case 'h': case '?': default: return $this->usage(); break; } } if (false === $verbose) { echo implode("\n", $this->commands); } Console\Cursor::colorize('foreground(yellow)'); echo static::LOGO; Console\Cursor::colorize('normal'); echo "\n\n", 'Just type:', "\n\n", ' $ katana <command> <options>', "\n\n", 'where <command> is:', "\n\n", ' * ', implode(',' . "\n" . ' * ', $this->commands), '.', "\n\n", '<options> always contains -h, -? and --help to get the usage ' . 'of the command.', "\n"; }
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); }
/** * Colorize a portion of a text. * It is kind of a shortcut of \Hoa\Console\Color. * * @access public * @param string $text Text. * @param string $attributesBefore Style to apply. * @param string $attributesAfter Reset style. * @return string */ public static function colorize($text, $attributesBefore, $attributesAfter = 'normal') { ob_start(); \Hoa\Console\Cursor::colorize($attributesBefore); echo $text; \Hoa\Console\Cursor::colorize($attributesAfter); $out = ob_get_contents(); ob_end_clean(); return $out; }
public function _bindK() { if ($this->columnOffset == $this->table->getNumberOfColumns() - 1) { Cursor::bip(); } else { $this->columnOffset++; $this->renderInternal(); } return Readline::STATE_NO_ECHO; }
/** * The entry method. * * @return int */ public function main() { $exists = true; $unfold = false; $tree = false; $verbose = Console::isDirect(STDOUT); while (false !== ($c = $this->getOption($v))) { switch ($c) { case 'E': $exists = false; break; case 'u': $unfold = true; break; case 't': $tree = true; break; case 'V': $verbose = false; break; case 'h': case '?': return $this->usage(); case '__ambiguous': $this->resolveOptionAmbiguity($v); break; } } $this->parser->listInputs($path); if (null === $path) { return $this->usage(); } if (true === $tree) { $protocol = Protocol::getInstance(); $foo = substr($path, 0, 6); if ('hoa://' !== $foo) { return; } $path = substr($path, 6); $current = $protocol; foreach (explode('/', $path) as $component) { if (!isset($current[$component])) { break; } $current = $current[$component]; } echo $current; return; } if (true === $verbose) { echo Console\Cursor::colorize('foreground(yellow)'), $path, Console\Cursor::colorize('normal'), ' is equivalent to:', "\n"; } $resolved = resolve($path, $exists, $unfold); foreach ((array) $resolved as $r) { echo $r, "\n"; } return; }
/** * 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); }
/** * The entry method. * * @return int */ public function main() { $breakBC = false; $minimumTag = null; $doSteps = ['test' => -1, 'changelog' => -1, 'tag' => -1, 'github' => -1]; $onlyStep = function ($step) use(&$doSteps) { $doSteps[$step] = 1; foreach ($doSteps as &$doStep) { if (-1 === $doStep) { $doStep = 0; } } return; }; while (false !== ($c = $this->getOption($v))) { switch ($c) { case '__ambiguous': $this->resolveOptionAmbiguity($v); break; case 'c': $onlyStep('changelog'); break; case 't': $onlyStep('tag'); break; case 'g': $onlyStep('github'); break; case 'b': $breakBC = $v; break; case 'm': $minimumTag = $v; break; case 'h': case '?': default: return $this->usage(); } } $this->parser->listInputs($repositoryRoot); if (empty($repositoryRoot)) { return $this->usage(); } if (false === file_exists($repositoryRoot . DS . '.git')) { throw new Console\Exception('%s is not a valid Git repository.', 0, $repositoryRoot); } date_default_timezone_set('UTC'); $allTags = $tags = explode("\n", Console\Processus::execute('git --git-dir=' . $repositoryRoot . '/.git ' . 'tag')); rsort($tags); list($currentMCN) = explode('.', $tags[0], 2); if (true === $breakBC) { ++$currentMCN; } $newTag = $currentMCN . '.' . date('y.m.d'); if (null === $minimumTag) { $tags = [$tags[0]]; } else { $toInt = function ($tag) { list($x, $y, $m, $d) = explode('.', $tag); return $x * 1000000 + $y * 10000 + $m * 100 + $d * 1; }; $_tags = []; $_minimumTag = $toInt($minimumTag); foreach ($tags as $tag) { if ($toInt($tag) >= $_minimumTag) { $_tags[] = $tag; } } $tags = $_tags; } $changelog = ''; echo 'We are going to snapshot this library together, by following ', 'these steps:', "\n", ' 1. tests must pass,', "\n", ' 2. updating the CHANGELOG.md file,', "\n", ' 3. commit the CHANGELOG.md file,', "\n", ' 4. creating a tag,', "\n", ' 5. pushing the tag,', "\n", ' 6. creating a release on Github.', "\n"; $step = function ($stepGroup, $message, $task) use($doSteps) { echo "\n\n"; Console\Cursor::colorize('foreground(black) background(yellow)'); echo 'Step “', $message, '”.'; Console\Cursor::colorize('normal'); echo "\n"; if (0 === $doSteps[$stepGroup]) { $answer = 'no'; } else { $answer = $this->readLine('Would you like to do this one: [yes/no] '); } if ('yes' === $answer) { echo "\n"; $task(); } else { Console\Cursor::colorize('foreground(red)'); echo 'Aborted!', "\n"; Console\Cursor::colorize('normal'); } }; $step('test', 'tests must pass', function () { echo 'Tests must be green. Execute:', "\n", ' $ hoa test:run -d Test', "\n", 'to run the tests.', "\n"; $this->readLine('Press Enter when it is green (or Ctrl-C to abort).'); }); $step('changelog', 'updating the CHANGELOG.md file', function () use($tags, $newTag, $repositoryRoot, &$changelog) { array_unshift($tags, 'HEAD'); $changelog = null; for ($i = 0, $max = count($tags) - 1; $i < $max; ++$i) { $fromStep = $tags[$i]; $toStep = $tags[$i + 1]; $title = $fromStep; if ('HEAD' === $fromStep) { $title = $newTag; } $changelog .= '# ' . $title . "\n\n" . Console\Processus::execute('git --git-dir=' . $repositoryRoot . '/.git ' . 'log ' . '--first-parent ' . '--pretty="format: * %s (%aN, %aI)" ' . $fromStep . '...' . $toStep, false) . "\n\n"; } $file = new File\ReadWrite($repositoryRoot . DS . 'CHANGELOG.md'); $file->rewind(); $temporary = new File\ReadWrite($repositoryRoot . DS . '._hoa.CHANGELOG.md'); $temporary->truncate(0); $temporary->writeAll($changelog); $temporary->close(); echo 'The CHANGELOG is ready.', "\n"; $this->readLine('Press Enter to check and edit the file (empty the file to abort).'); Console\Chrome\Editor::open($temporary->getStreamName()); $temporary->open(); $changelog = $temporary->readAll(); if (empty(trim($changelog))) { $temporary->delete(); $temporary->close(); exit; } $previous = $file->readAll(); $file->truncate(0); $file->writeAll($changelog . $previous); $temporary->delete(); $temporary->close(); $file->close(); return; }); $step('changelog', 'commit the CHANGELOG.md file', function () use($newTag, $repositoryRoot) { echo Console\Processus::execute('git --git-dir=' . $repositoryRoot . '/.git ' . 'add ' . '--verbose ' . 'CHANGELOG.md'); echo Console\Processus::execute('git --git-dir=' . $repositoryRoot . '/.git ' . 'commit ' . '--verbose ' . '--message "Prepare ' . $newTag . '." ' . 'CHANGELOG.md'); return; }); $step('tag', 'creating a tag', function () use($breakBC, $step, $currentMCN, $repositoryRoot, $tags, $newTag, $allTags) { if (true === $breakBC) { echo 'A BC break has been introduced, ', 'few more steps are required:', "\n"; $step('tag', 'update the composer.json file', function () use($currentMCN) { echo 'The `extra.branch-alias.dev-master` value ', 'must be set to `', $currentMCN, '.x-dev`', "\n"; $this->readLine('Press Enter to edit the file.'); Console\Chrome\Editor::open($repository . DS . 'composer.json'); }); $step('tag', 'open issues to update parent dependencies', function () { echo 'Some libraries may depend on this one. ', 'Issues must be opened to update this ', 'dependency.', "\n"; $this->readLine('Press Enter when it is done (or Ctrl-C to abort).'); }); $step('tag', 'update the README.md file', function () use($currentMCN) { echo 'The installation Section must invite the ', 'user to install the version ', '`~', $currentMCN, '.0`.', "\n"; $this->readLine('Press Enter when it is done (or Ctrl-C to abort).'); Console\Chrome\Editor::open($repository . DS . 'README.md'); }); $step('tag', 'commit the composer.json and README.md files', function () use($currentMCN) { echo Console\Processus::execute('git --git-dir=' . $repositoryRoot . '/.git ' . 'add ' . '--verbose ' . 'composer.json README.md'); echo Console\Processus::execute('git --git-dir=' . $repositoryRoot . '/.git ' . 'commit ' . '--verbose ' . '--message "Update because of the BC break." ' . 'composer.json README.md'); }); } $status = Console\Processus::execute('git --git-dir=' . $repositoryRoot . '/.git ' . 'status ' . '--short'); if (!empty($status)) { Console\Cursor::colorize('foreground(white) background(red)'); echo 'At least one file is not commited!'; Console\Cursor::colorize('normal'); echo "\n", '(tips: use `git stash` if it is not related ', 'to this snapshot)', "\n"; $this->readLine('Press Enter when everything is clean.'); } echo 'Here is the list of tags:', "\n", ' * ', implode(',' . "\n" . ' * ', $allTags), '.', "\n", 'We are going to create the following tag: ', $newTag, '.', "\n"; $answer = $this->readLine('Is it correct? [yes/no] '); if ('yes' !== $answer) { return; } Console\Processus::execute('git --git-dir=' . $repositoryRoot . '/.git ' . 'tag ' . $newTag); }); $step('tag', 'push the new snapshot', function () use($repositoryRoot) { Console\Cursor::colorize('foreground(white) background(red)'); echo 'This step ', Console\Cursor::colorize('underlined'); echo 'must not'; Console\Cursor::colorize('!underlined'); echo ' be undo!'; Console\Cursor::colorize('normal'); echo "\n"; $i = 5; while ($i-- > 0) { Console\Cursor::clear('↔'); echo $i + 1; sleep(1); } $remotes = Console\Processus::execute('git --git-dir=' . $repositoryRoot . '/.git ' . 'remote ' . '--verbose'); $gotcha = false; foreach (explode("\n", $remotes) as $remote) { if (0 !== preg_match('/(git@git.hoa-project.net:[^ ]+)/', $remote, $matches)) { $gotcha = true; break; } } if (false === $gotcha) { echo 'No remote has been found.'; return; } echo "\n", 'To push tag, execute:', "\n", ' $ git push ', $matches[1], "\n", ' $ git push ', $matches[1], ' --tags', "\n"; $this->readLine('Press Enter when it is done (or Ctrl-C to abort).'); }); $step('github', 'create a release on Github', function () use($newTag, $changelog, $repositoryRoot) { $temporary = new File\ReadWrite($repositoryRoot . DS . '._hoa.GithubRelease.md'); $temporary->truncate(0); if (!empty($changelog)) { $temporary->writeAll($changelog); } $temporary->close(); Console\Chrome\Editor::open($temporary->getStreamName()); $temporary->open(); $temporary->rewind(); $body = $temporary->readAll(); $temporary->delete(); $composer = json_decode(file_get_contents('composer.json')); list(, $libraryName) = explode('/', $composer->name); $output = json_encode(['tag_name' => $newTag, 'body' => $body]); $username = $this->readLine('Username: '******'Password: '******':' . $password); $context = stream_context_create(['http' => ['method' => 'POST', 'header' => 'Host: api.github.com' . CRLF . 'User-Agent: Hoa\\Devtools' . CRLF . 'Accept: application/json' . CRLF . 'Content-Type: application/json' . CRLF . 'Content-Length: ' . strlen($output) . CRLF . 'Authorization: Basic ' . $auth . CRLF, 'content' => $output]]); echo file_get_contents('https://api.github.com/repos/hoaproject/' . $libraryName . '/releases', false, $context); }); echo "\n", '🍺 🍺 🍺', "\n"; return; }
/** * */ private function drawField() { \Hoa\Console\Cursor::clear(); //\Hoa\Console\Cursor::save(); //echo PHP_EOL; //\Hoa\Console\Cursor::colorize('underlined foreground(yellow) background(#932e2e)'); echo $this->view->drawField(); //\Hoa\Console\Cursor::restore(); }
break; } } } } Core::enableErrorHandler(); Core::enableExceptionHandler(); /** * Here we go! */ try { $router = new Router\Cli(); $router->get('g', '(?:(?<vendor>\\w+)\\s+)?(?<library>\\w+)?(?::(?<command>\\w+))?(?<_tail>.*?)', 'main', 'main', ['vendor' => 'hoa', 'library' => 'core', 'command' => 'welcome']); $dispatcher = new Dispatcher\ClassMethod(['synchronous.call' => '(:%variables.vendor:lU:)\\(:%variables.library:lU:)\\Bin\\(:%variables.command:lU:)', 'synchronous.able' => 'main']); $dispatcher->setKitName('Hoa\\Console\\Dispatcher\\Kit'); exit((int) $dispatcher->dispatch($router)); } catch (Core\Exception $e) { $message = $e->raise(true); $code = 1; } catch (\Exception $e) { $message = $e->getMessage(); $code = 2; } ob_start(); Console\Cursor::colorize('foreground(white) background(red)'); echo $message, "\n"; Console\Cursor::colorize('normal'); $content = ob_get_contents(); ob_end_clean(); file_put_contents('php://stderr', $content); exit($code);
/** * Clear the current line/buffer. * ctrl+c like behavior, used by Reader * @param int|object $code SIGINT = 2 on osx */ public function bindClearLine($code = null) { $line = $this->reader->getLine(); $buffer = trim($this->reader->getBuffer()); // ctrl+c = Cancelling... if (!empty($line) && $buffer === '') { Cursor::clear('line'); } else { // User is viewing their history, else typed something if (empty($this->buf)) { $mLines = substr_count($buffer, "\n"); } else { $mLines = substr_count($this->buf, "\n"); } // Redraw the prompt, by erasing the existing buffered lines Cursor::move('up', $mLines); Cursor::clear('line'); Cursor::clear('down', $mLines); $this->buf = ''; echo $this->prompter; } $this->reader->setLine(null); $this->reader->setBuffer(''); }
/** * The entry method. * * @return int */ public function main() { while (false !== ($c = $this->getOption($v))) { switch ($c) { case 'h': case '?': return $this->usage(); case '__ambiguous': $this->resolveOptionAmbiguity($v); break; } } Realdom::setDefaultSampler(new Math\Sampler\Random()); $compiler = Compiler\Llk::load(new File\Read('hoa://Library/Praspel/Grammar.pp')); $interpreter = new Praspel\Visitor\Interpreter(); $dump = new Praspel\Visitor\Compiler(); $interpreter->visit($compiler->parse('@requires;')); $words = []; from('Hoathis or Hoa')->foreachImport('Realdom.*', function ($classname) use(&$words) { $class = new \ReflectionClass($classname); if ($class->isSubclassOf('\\Hoa\\Realdom')) { $words[] = $classname::NAME; } return; }); $readline = new Console\Readline(); $readline->setAutocompleter(new Console\Readline\Autocompleter\Word($words)); $expression = '.h'; do { try { if ('.' === $expression[0]) { @(list($expression, $tail) = explode(' ', $expression)); } switch ($expression) { case '.h': case '.help': echo 'Usage:', "\n", ' .h[elp] to print this help;', "\n", ' .c[lear] to clear the screen;', "\n", ' .v[ariables] to print all variables;', "\n", ' .s[ample] to sample a value of a variable;', "\n", ' .u[nset] to unset a variable;', "\n", ' .d[ump] to dump the tree of the expression;', "\n", ' .q[uit] to quit.', "\n"; break; case '.c': case '.clear': Console\Cursor::clear('↕'); break; case '.v': case '.variables': foreach ($interpreter->getClause() as $variable) { echo $variable->getName(), ': ', $variable->getHeld()->toPraspel(), "\n"; } break; case '.s': case '.sample': if (null === $tail) { echo 'You must precise a variable name.', "\n"; break; } $_clause = $interpreter->getClause(); if (!isset($_clause[$tail])) { echo 'Variable ', $tail, ' does not exist.', "\n"; break; } $_variable = $_clause[$tail]; var_export($_variable->sample()); echo "\n"; $_variable->reset(); break; case '.u': case '.unset': if (null === $tail) { echo 'You must precise a variable name.', "\n"; break; } $_clause = $interpreter->getClause(); unset($_clause[$tail]); break; case '.d': case '.dump': echo $dump->visit($interpreter->getRoot()); break; case '.q': case '.quit': break 2; default: if (null === $expression) { break; } $interpreter->visit($compiler->parse($expression, 'expression')); break; } } catch (\Exception $e) { echo $e->getMessage(), "\n"; } echo "\n"; } while (false !== ($expression = $readline->readLine('> '))); return; }
/** * 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; }
/** * The entry method. * * @return int */ public function main() { $libraries = []; while (false !== ($c = $this->getOption($v))) { switch ($c) { case 'a': $iterator = new File\Finder(); $iterator->in(resolve('hoa://Library/', true, true))->directories()->maxDepth(1); foreach ($iterator as $fileinfo) { $libraryName = $fileinfo->getBasename(); $pathname = resolve('hoa://Library/' . $libraryName); $automaticTests = $pathname . DS . 'Test' . DS . 'Praspel' . DS; if (is_dir($automaticTests)) { $libraries[] = $automaticTests; } } if (empty($libraries)) { echo 'Already clean.'; return; } break; case 'l': foreach ($this->parser->parseSpecialValue($v) as $library) { $libraryName = ucfirst(strtolower($library)); $pathname = resolve('hoa://Library/' . $libraryName); $automaticTests = $pathname . DS . 'Test' . DS . 'Praspel' . DS; if (is_dir($automaticTests)) { $libraries[] = $automaticTests; } } if (empty($libraries)) { echo 'Already clean.'; return; } break; case '__ambiguous': $this->resolveOptionAmbiguity($v); break; case 'h': case '?': default: return $this->usage(); } } if (empty($libraries)) { return $this->usage(); } foreach ($libraries as $path) { $status = 'Clean ' . (40 < strlen($path) ? '…' . substr($path, -39) : $path); echo ' ⌛ ', $status; $directory = new File\Directory($path); if (false === $directory->delete()) { echo ' ', Console\Chrome\Text::colorize('✖︎', 'foreground(red)'), ' ', $status, "\n"; } else { Console\Cursor::clear('↔'); echo ' ', Console\Chrome\Text::colorize('✔︎', 'foreground(green)'), ' ', $status, "\n"; } $directory->close(); } return; }
/** * 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(); } }
/** * The entry method. * * @return int */ public function main() { $dryRun = false; $classes = []; while (false !== ($c = $this->getOption($v))) { switch ($c) { case 'n': foreach ($this->parser->parseSpecialValue($v) as $namespace) { $namespace = trim(str_replace('.', '\\', $namespace), '\\'); if (false === ($pos = strpos($namespace, '\\'))) { throw new Console\Exception('Namespace %s is too short.', 0, $namespace); } $tail = substr($namespace, strpos($namespace, '\\') + 1); $root = resolve('hoa://Library/' . str_replace('\\', '/', $tail)); $classes = array_merge($classes, static::findClasses($root, $namespace)); } break; case 'c': foreach ($this->parser->parseSpecialValue($v) as $class) { $classes[] = $class; } break; case 'd': $dryRun = $v; break; case '__ambiguous': $this->resolveOptionAmbiguity($v); break; case 'h': case '?': default: return $this->usage(); } } if (empty($classes)) { return $this->usage(); } foreach ($classes as $i => $class) { $classes[$i] = str_replace('.', '\\', $class); } $generator = new \Atoum\PraspelExtension\Praspel\Generator(); $generator->setTestNamespacer(function ($namespace) { $parts = explode('\\', $namespace); return implode('\\', array_slice($parts, 0, 2)) . '\\Test\\Praspel\\Unit' . (isset($parts[2]) ? '\\' . implode('\\', array_slice($parts, 2)) : ''); }); $phpBinary = Core::getPHPBinary() ?: Console\Processus::localte('php'); $envVariable = '__HOA_ATOUM_PRASPEL_EXTENSION_' . md5(Core::uuid()); $reflection = null; $buffer = null; $reflectionner = new Console\Processus($phpBinary); $reflectionner->on('input', function (Core\Event\Bucket $bucket) use($envVariable) { $bucket->getSource()->writeAll('<?php' . "\n" . 'require_once \'' . dirname(__DIR__) . DS . '.bootstrap.atoum.php\';' . "\n" . '$class = getenv(\'' . $envVariable . '\');' . "\n" . 'if (class_exists(\'\\mageekguy\\atoum\\scripts\\runner\', false)) {' . "\n" . ' \\atoum\\scripts\\runner::disableAutorun();' . "\n" . '}' . "\n" . '$reflection = new \\Atoum\\PraspelExtension\\Praspel\\Reflection\\RClass($class);' . "\n" . 'echo serialize($reflection), "\\n";'); return false; }); $reflectionner->on('output', function (Core\Event\Bucket $bucket) use(&$buffer) { $data = $bucket->getData(); $buffer .= $data['line'] . "\n"; return; }); $reflectionner->on('stop', function () use(&$buffer, &$reflection) { $handle = @unserialize($buffer); if (false === $handle) { echo $buffer, "\n"; return; } $reflection = $handle; return; }); foreach ($classes as $class) { $status = $class . ' (in '; echo ' ⌛ ', $status; putenv($envVariable . '=' . $class); $buffer = null; $reflection = null; $reflectionner->run(); $output = $generator->generate($reflection); $parts = explode('\\', $class); $paths = resolve('hoa://Library/' . $parts[1] . '/' . 'Test/Praspel/Unit/' . implode('/', array_slice($parts, 2)) . '.php', false, true); $max = 0; $thePath = 0; foreach ($paths as $path) { $length = Ustring\Search::lcp($reflection->getFilename(), $path); if ($length > $max) { $thePath = $path; } } $statusTail = (40 < strlen($thePath) ? '…' . substr($thePath, -39) : $thePath) . ')'; echo $statusTail; $status .= $statusTail; if (false === $reflection->isInstantiable()) { Console\Cursor::clear('↔'); echo ' ⚡️ ', $status, '; not instantiable.', "\n"; continue; } $dirname = dirname($thePath); if (false === is_dir($dirname)) { if (false === $dryRun) { mkdir($dirname, 0755, true); } else { echo "\n", static::info('Creating directory: ' . $dirname . '.'); } } if (false === $dryRun) { file_put_contents($thePath, $output); } else { echo "\n", static::info('Content of the ' . $thePath . ':'), "\n"; Console\Cursor::colorize('foreground(yellow)'); echo ' ┏', "\n", ' ┃ ', str_replace("\n", "\n" . ' ┃ ', trim($output)), "\n", ' ┗', "\n"; Console\Cursor::colorize('foreground(normal)'); } Console\Cursor::clear('↔'); echo ' ', Console\Chrome\Text::colorize('✔︎', 'foreground(green)'), ' ', $status, "\n"; } return; }
/** * The entry method. * * @return int */ public function main() { $directories = []; $clean = false; $lang = 'En'; while (false !== ($c = $this->getOption($v))) { switch ($c) { case 'd': foreach ($this->parser->parseSpecialValue($v) as $directory) { $directory = realpath($directory); if (false === is_dir($directory)) { throw new Console\Exception('Directory %s does not exist.', 0, $directory); } $directories[] = $directory; } break; case 'c': $clean = true; break; case 'l': $lang = ucfirst(strtolower($v)); break; case '__ambiguous': $this->resolveOptionAmbiguity($v); break; case 'h': case '?': default: return $this->usage(); } } $workspace = resolve('hoa://Library/Devtools/Resource/Documentation') . DS . 'HackBook.output'; if (true === $clean) { if (true === is_dir($workspace)) { $directory = new File\Directory($workspace); $directory->delete(); unset($directory); } return; } clearstatcache(true); $workspace .= DS . $lang; if (false === is_dir($workspace)) { File\Directory::create($workspace); } Console\Cursor::colorize('foreground(yellow)'); echo 'Selected language: ', $lang, '.', "\n\n"; Console\Cursor::colorize('normal'); require_once 'hoa://Library/Devtools/Resource/Documentation/Router.php'; // $router is defined. $finder = new File\Finder(); foreach ($directories as $location) { $_location = $location . DS . 'Documentation' . DS . $lang; if (false === is_dir($_location)) { throw new Console\Exception('Directory %s does not contain documentation.', 1, $location); } $finder->in($_location); } foreach (resolve('hoa://Library', true, true) as $location) { $libraryFinder = new File\Finder(); $libraryFinder->in($location)->directories()->maxDepth(1); foreach ($libraryFinder as $_location) { $_location = $_location->getPathName() . DS . 'Documentation' . DS . $lang; if (true === is_dir($_location)) { $finder->in($_location); } } } $vendors = []; foreach ($finder as $entry) { $path = dirname(dirname($entry->getPath())); $vendor = ucfirst(strtolower(basename(dirname($path)))); $library = ucfirst(strtolower(basename($path))); if (!isset($vendors[$vendor])) { $vendors[$vendor] = []; } $vendors[$vendor][$library] = ['library' => $library, 'vendor' => $vendor, 'fullname' => $vendor . '\\' . $library]; } foreach ($vendors as $vendor => &$libraries) { $libraries = array_values($libraries); } $layout = new File\Read('hoa://Library/Devtools/Resource/Documentation/Layout.xyl'); $xyl = new Xyl($layout, new File\Write($workspace . '/index.html'), new Xyl\Interpreter\Html(), $router); $xyl->setTheme(''); $data = $xyl->getData(); foreach ($vendors as $vendor => $libraries) { $data->vendors->vendor = ['name' => $vendor, 'library' => $libraries]; } $xyl->addOverlay('hoa://Library/Devtools/Resource/Documentation/Index.xyl'); $xyl->render(); echo 'Generate', "\t"; Console\Cursor::colorize('foreground(green)'); echo 'index.html'; Console\Cursor::colorize('normal'); echo '.', "\n"; $xyl = null; foreach ($vendors as $vendor => $libraries) { File\Directory::create($workspace . dirname($router->unroute('full', ['vendor' => $libraries[0]['vendor'], 'chapter' => $libraries[0]['library']]))); foreach ($libraries as $library) { $in = 'hoa://Library/' . $library['library'] . '/Documentation/' . $lang . '/Index.xyl'; $out = $workspace . $router->unroute('full', ['vendor' => $library['vendor'], 'chapter' => $library['library']]); if (true === file_exists($out) && filemtime($in) <= filemtime($out)) { echo 'Skip', "\t\t"; Console\Cursor::colorize('foreground(green)'); echo $library['fullname']; Console\Cursor::colorize('normal'); echo '.', "\n"; continue; } $out = new File\Write($out); $out->truncate(0); if (null === $xyl) { $xyl = new Xyl($layout, $out, new Xyl\Interpreter\Html(), $router); $xyl->setTheme(''); $xyl->addOverlay('hoa://Library/Devtools/Resource/Documentation/Chapter.xyl'); } else { $xyl->setOutputStream(new File\Write($out)); } $xyl->addOverlay($in); $xyl->getData()->name[0] = $library['fullname']; $xyl->getData()->library[0] = $library['library']; try { $xyl->render(); } catch (\Exception $e) { echo $e->getMessage(), "\n"; } $xyl->removeOverlay($in); echo 'Generate', "\t"; Console\Cursor::colorize('foreground(green)'); echo $library['fullname']; Console\Cursor::colorize('normal'); echo '.', "\n"; } } echo "\n", 'Open file://', $workspace, '/index.html', '.', "\n"; return; }
/** * 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; } }