public function main() { $root = '.'; $address = '127.0.0.1:8888'; while (false !== ($c = $this->getOption($v))) { switch ($c) { case 'r': $root = $v; break; case 'a': $address = $v; break; case 'h': case '?': return $this->usage(); break; case '__ambiguous': $this->resolveOptionAmbiguity($v); break; } } if (false === is_dir($root)) { throw new Console\Exception('Root %s is not a directory.', 0, $root); } $processus = new Console\Processus(PHP_BINARY, ['-S' => $address, '-t' => $root]); $processus->on('input', function () { return false; }); $processus->on('output', function (Event\Bucket $bucket) { echo $bucket->getData()['line'], "\n"; }); echo 'Server is listening ', $root, ' on ', $address, '.', "\n\n"; $processus->run(); return; }
/** * Open an editor. * * @param string $file File to open. * @param string $editor Editor to use ($_SERVER['EDITOR'] by * default). * @return string */ public static function open($file = '', $editor = null) { if (null === $editor) { if (isset($_SERVER['EDITOR'])) { $editor = $_SERVER['EDITOR']; } else { $editor = 'vi'; } } if (!empty($file)) { $file = escapeshellarg($file); } return Console\Processus::execute($editor . ' ' . $file . ' > `tty` < `tty`', false); }
/** * Restore previous interaction options. * * @return void */ public static function restoreInteraction() { if (null === self::$_old) { return; } Processus::execute('stty ' . self::$_old); 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; $diff = false; while (false !== ($c = $this->getOption($v))) { switch ($c) { case '__ambiguous': $this->resolveOptionAmbiguity($v); break; case 'd': $dryRun = true; break; case 'D': $diff = true; break; case 'h': case '?': default: return $this->usage(); } } $this->parser->listInputs($path); if (empty($path)) { return $this->usage(); } $phpCsFixer = Console\Processus::locate('php-cs-fixer'); $configurationFile = resolve('hoa://Library/Devtools/Resource/PHPCSFixer/ConfigurationFile.php'); if (empty($phpCsFixer)) { throw new Console\Exception('php-cs-fixer binary is not found.', 0); } $arguments = ['fix', '--config-file' => $configurationFile]; if (true === $dryRun) { $arguments[] = '--dry-run'; } if (true === $diff) { $arguments[] = '--diff'; } $arguments[] = $path; $processus = new Console\Processus($phpCsFixer, $arguments); $processus->on('input', function () { return false; }); $processus->on('output', function (Core\Event\Bucket $bucket) { echo $bucket->getData()['line'], "\n"; return; }); $processus->run(); return; }
/** * 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; }
/** * The entry method. * * @return int */ public function main() { $verbose = Console::isDirect(STDOUT); $printSnapshot = false; $printDays = false; $printCommits = false; while (false !== ($c = $this->getOption($v))) { switch ($c) { case '__ambiguous': $this->resolveOptionAmbiguity($v); break; case 'V': $verbose = false; break; case 's': $printSnapshot = true; break; case 'd': $printDays = true; break; case 'c': $printCommits = true; 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); } $tag = Console\Processus::execute('git --git-dir=' . $repositoryRoot . '/.git ' . 'describe --abbrev=0 --tags origin/master'); if (empty($tag)) { throw new Console\Exception('No tag.', 1); } $timeZone = new \DateTimeZone('UTC'); $snapshotDT = \DateTime::createFromFormat('*.y.m.d', $tag, $timeZone); $sixWeeks = new \DateInterval('P6W'); $nextSnapshotDT = clone $snapshotDT; $nextSnapshotDT->add($sixWeeks); $today = new \DateTime('now', $timeZone); $needNewSnapshot = '+' === $nextSnapshotDT->diff($today)->format('%R'); $numberOfDays = 0; $numberOfCommits = 0; $output = 'No snapshot is required.'; if (true === $needNewSnapshot) { $numberOfDays = (int) $nextSnapshotDT->diff($today)->format('%a'); $numberOfCommits = (int) Console\Processus::execute('git --git-dir=' . $repositoryRoot . '/.git ' . 'rev-list ' . $tag . '..origin/master --count'); $needNewSnapshot = 0 < $numberOfCommits; if (true === $needNewSnapshot) { $output = 'A snapshot is required, since ' . $numberOfDays . ' day' . (1 < $numberOfDays ? 's' : '') . ' (tag ' . $tag . ', ' . $numberOfCommits . ' commit' . (1 < $numberOfCommits ? 's' : '') . ' to publish)!'; } } if (true === $printSnapshot || true === $printDays || true === $printCommits) { $columns = []; if (true === $printSnapshot) { $columns[] = $tag; } if (true === $printDays) { $columns[] = $numberOfDays . ' day' . (1 < $numberOfDays ? 's' : ''); } if (true === $printCommits) { $columns[] = $numberOfCommits . ' commit' . (1 < $numberOfCommits ? 's' : ''); } echo implode("\t", $columns), "\n"; } elseif (true === $verbose) { echo $output, "\n"; } return !$needNewSnapshot; }
/** * Use pager set in the environment (i.e. $_ENV['PAGER']). * * @param string $output Output (from the output buffer). * @param int $mode Mode (from the output buffer). * @param string $type Type. Please, see self::LESS or self::MORE. * @return string */ public static function pager($output, $mode, $type = null) { static $process = null; if ($mode & PHP_OUTPUT_HANDLER_START) { $pager = null !== $type ? Console\Processus::locate($type) : (isset($_ENV['PAGER']) ? $_ENV['PAGER'] : null); if (null === $pager) { return $output; } $process = new Console\Processus($pager, null, [['pipe', 'r']]); $process->open(); } $process->writeAll($output); if ($mode & PHP_OUTPUT_HANDLER_FINAL) { $process->close(); } return null; }
protected function execute($pharName, $options) { return Processus::execute($this->getPhpPath() . ' -d phar.readonly=1 ' . $pharName . ' ' . $options); }
/** * 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; }
/** * Get current size (x and y) of the window. * * @return array */ public static function getSize() { if (OS_WIN) { $modecon = explode("\n", ltrim(Processus::execute('mode con'))); $_y = trim($modecon[2]); preg_match('#[^:]+:\\s*([0-9]+)#', $_y, $matches); $y = (int) $matches[1]; $_x = trim($modecon[3]); preg_match('#[^:]+:\\s*([0-9]+)#', $_x, $matches); $x = (int) $matches[1]; return ['x' => $x, 'y' => $y]; } $term = ''; if (isset($_SERVER['TERM'])) { $term = 'TERM="' . $_SERVER['TERM'] . '" '; } $command = $term . 'tput cols && ' . $term . 'tput lines'; $tput = Processus::execute($command, false); if (!empty($tput)) { list($x, $y) = explode("\n", $tput); return ['x' => intval($x), 'y' => intval($y)]; } // DECSLPP. echo "[18t"; // Read \033[8;y;xt. fread(STDIN, 4); // skip \033, [, 8 and ;. $x = null; $y = null; $handle =& $y; do { $char = fread(STDIN, 1); switch ($char) { case ';': $handle =& $x; break; case 't': break 2; default: if (false === ctype_digit($char)) { break 2; } $handle .= $char; } } while (true); if (null === $x || null === $y) { return ['x' => 0, 'y' => 0]; } return ['x' => (int) $x, 'y' => (int) $y]; }