Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Console] Add Cursor class to control the cursor in the terminal #27444

Merged
merged 1 commit into from Apr 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it really belong to the cursor?


/**
* 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