Skip to content

Commit

Permalink
[Console] Add Cursor class to control the cursor in the terminal
Browse files Browse the repository at this point in the history
  • Loading branch information
pierredup authored and fabpot committed Apr 12, 2020
1 parent 4dabd00 commit 80d59d5
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 14 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Component/Console/CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@ CHANGELOG

* `Command::setHidden()` is final since Symfony 5.1
* Add `SingleCommandApplication`
* Add `Cursor` class

5.0.0
-----
Expand Down
137 changes: 137 additions & 0 deletions src/Symfony/Component/Console/Cursor.php
@@ -0,0 +1,137 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Console;

use Symfony\Component\Console\Output\OutputInterface;

/**
* @author Pierre du Plessis <pdples@gmail.com>
*/
class Cursor
{
private $output;

private $input;

public function __construct(OutputInterface $output, $input = STDIN)
{
$this->output = $output;
$this->input = $input;
}

public function moveUp(int $lines = 1)
{
$this->output->write(sprintf("\x1b[%dA", $lines));
}

public function moveDown(int $lines = 1)
{
$this->output->write(sprintf("\x1b[%dB", $lines));
}

public function moveRight(int $columns = 1)
{
$this->output->write(sprintf("\x1b[%dC", $columns));
}

public function moveLeft(int $columns = 1)
{
$this->output->write(sprintf("\x1b[%dD", $columns));
}

public function moveToColumn(int $column)
{
$this->output->write(sprintf("\x1b[%dG", $column));
}

public function moveToPosition(int $column, int $row)
{
$this->output->write(sprintf("\x1b[%d;%dH", $row + 1, $column));
}

public function savePosition()
{
$this->output->write("\x1b7");
}

public function restorePosition()
{
$this->output->write("\x1b8");
}

public function hide()
{
$this->output->write("\x1b[?25l");
}

public function show()
{
$this->output->write("\x1b[?25h\x1b[?0c");
}

/**
* Clears all the output from the current line.
*/
public function clearLine(bool $fromCurrentPosition = false)
{
if (true === $fromCurrentPosition) {
$this->output->write("\x1b[K");
} else {
$this->output->write("\x1b[2K");
}
}

/**
* Clears all the output from the cursors' current position to the end of the screen.
*/
public function clearOutput()
{
$this->output->write("\x1b[0J", false);
}

/**
* Clears the entire screen.
*/
public function clearScreen()
{
$this->output->write("\x1b[2J", false);
}

/**
* Returns the current cursor position as x,y coordinates.
*/
public function getCurrentPosition(): array
{
static $isTtySupported;

if (null === $isTtySupported && \function_exists('proc_open')) {
$isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes);
}

if (!$isTtySupported) {
return [1, 1];
}

$sttyMode = shell_exec('stty -g');
shell_exec('stty -icanon -echo');

@fwrite($this->input, "\033[6n");

$code = trim(fread($this->input, 1024));

shell_exec(sprintf('stty %s', $sttyMode));

sscanf($code, "\033[%d;%dR", $row, $col);

return [$col, $row];
}
}
10 changes: 6 additions & 4 deletions src/Symfony/Component/Console/Helper/ProgressBar.php
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Cursor;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
Expand Down Expand Up @@ -47,6 +48,7 @@ final class ProgressBar
private $overwrite = true;
private $terminal;
private $previousMessage;
private $cursor;

private static $formatters;
private static $formats;
Expand Down Expand Up @@ -78,6 +80,7 @@ public function __construct(OutputInterface $output, int $max = 0, float $minSec
}

$this->startTime = time();
$this->cursor = new Cursor($output);
}

/**
Expand Down Expand Up @@ -462,13 +465,12 @@ private function overwrite(string $message): void
$lines = floor(Helper::strlen($message) / $this->terminal->getWidth()) + $this->formatLineCount + 1;
$this->output->clear($lines);
} else {
// Erase previous lines
if ($this->formatLineCount > 0) {
$message = str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount).$message;
$this->cursor->moveUp($this->formatLineCount);
}

// Move the cursor to the beginning of the line and erase the line
$message = "\x0D\x1B[2K$message";
$this->cursor->moveToColumn(1);
$this->cursor->clearLine();
}
}
} elseif ($this->step > 0) {
Expand Down
15 changes: 7 additions & 8 deletions src/Symfony/Component/Console/Helper/QuestionHelper.php
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Cursor;
use Symfony\Component\Console\Exception\MissingInputException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
Expand Down Expand Up @@ -235,6 +236,8 @@ protected function writeError(OutputInterface $output, \Exception $error)
*/
private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string
{
$cursor = new Cursor($output, $inputStream);

$fullChoice = '';
$ret = '';

Expand Down Expand Up @@ -262,8 +265,7 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu
} elseif ("\177" === $c) { // Backspace Character
if (0 === $numMatches && 0 !== $i) {
--$i;
// Move cursor backwards
$output->write(sprintf("\033[%dD", s($fullChoice)->slice(-1)->width(false)));
$cursor->moveLeft(s($fullChoice)->slice(-1)->width(false));

$fullChoice = self::substr($fullChoice, 0, $i);
}
Expand Down Expand Up @@ -351,17 +353,14 @@ function ($match) use ($ret) {
}
}

// Erase characters from cursor to end of line
$output->write("\033[K");
$cursor->clearLine(true);

if ($numMatches > 0 && -1 !== $ofs) {
// Save cursor position
$output->write("\0337");
$cursor->savePosition();
// Write highlighted text, complete the partially entered response
$charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)));
$output->write('<hl>'.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).'</hl>');
// Restore cursor position
$output->write("\0338");
$cursor->restorePosition();
}
}

Expand Down

0 comments on commit 80d59d5

Please sign in to comment.