Skip to content

Commit

Permalink
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 committed Feb 11, 2019
1 parent 5ed68ee commit 7501b0f
Show file tree
Hide file tree
Showing 2 changed files with 338 additions and 0 deletions.
130 changes: 130 additions & 0 deletions src/Symfony/Component/Console/Cursor.php
@@ -0,0 +1,130 @@
<?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;

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

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("\x1b[s");
}

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

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

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

/**
* Clears all the output from the of the current line.
*/
public function clearLine()
{
$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', array(array('file', '/dev/tty', 'r'), array('file', '/dev/tty', 'w'), array('file', '/dev/tty', 'w')), $pipes);
}

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

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

fwrite(STDIN, "\033[6n");

$code = trim(fread(STDIN, 1024));

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

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

return array($col, $row);
}
}
208 changes: 208 additions & 0 deletions src/Symfony/Component/Console/Tests/CursorTest.php
@@ -0,0 +1,208 @@
<?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\Tests;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Cursor;
use Symfony\Component\Console\Output\StreamOutput;

class CursorTest extends TestCase
{
protected $stream;

protected function setUp()
{
$this->stream = fopen('php://memory', 'r+');
}

protected function tearDown()
{
fclose($this->stream);
$this->stream = null;
}

public function testMoveUpOneLine()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->moveUp();

$this->assertEquals("\x1b[1A", $this->getOutputContent($output));
}

public function testMoveUpMultipleLines()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->moveUp(12);

$this->assertEquals("\x1b[12A", $this->getOutputContent($output));
}

public function testMoveDownOneLine()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->moveDown();

$this->assertEquals("\x1b[1B", $this->getOutputContent($output));
}

public function testMoveDownMultipleLines()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->moveDown(12);

$this->assertEquals("\x1b[12B", $this->getOutputContent($output));
}

public function testMoveLeftOneLine()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->moveLeft();

$this->assertEquals("\x1b[1D", $this->getOutputContent($output));
}

public function testMoveLeftMultipleLines()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->moveLeft(12);

$this->assertEquals("\x1b[12D", $this->getOutputContent($output));
}

public function testMoveRightOneLine()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->moveRight();

$this->assertEquals("\x1b[1C", $this->getOutputContent($output));
}

public function testMoveRightMultipleLines()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->moveRight(12);

$this->assertEquals("\x1b[12C", $this->getOutputContent($output));
}

public function testMoveToColumn()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->moveToColumn(6);

$this->assertEquals("\x1b[6G", $this->getOutputContent($output));
}

public function testMoveToPosition()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->moveToPosition(18, 16);

$this->assertEquals("\x1b[17;18H", $this->getOutputContent($output));
}

public function testClearLine()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->clearLine();

$this->assertEquals("\x1b[2K", $this->getOutputContent($output));
}

public function testSavePosition()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->savePosition();

$this->assertEquals("\x1b[s", $this->getOutputContent($output));
}

public function testHide()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->hide();

$this->assertEquals("\x1b[?25l", $this->getOutputContent($output));
}

public function testShow()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->show();

$this->assertEquals("\x1b[?25h\x1b[?0c", $this->getOutputContent($output));
}

public function testRestorePosition()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->restorePosition();

$this->assertEquals("\x1b[u", $this->getOutputContent($output));
}

public function testClearOutput()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->clearOutput();

$this->assertEquals("\x1b[0J", $this->getOutputContent($output));
}

public function testGetCurrentPosition()
{
$cursor = new Cursor($output = $this->getOutputStream());

$cursor->moveToPosition(10, 10);
$position = $cursor->getCurrentPosition();

$this->assertEquals("\x1b[11;10H", $this->getOutputContent($output));

$isTtySupported = (bool) @proc_open('echo 1 >/dev/null', array(array('file', '/dev/tty', 'r'), array('file', '/dev/tty', 'w'), array('file', '/dev/tty', 'w')), $pipes);

if ($isTtySupported) {
// When tty is supported, we can't validate the exact cursor position since it depends where the cursor is when the test runs.
// Instead we just make sure that it doesn't return 1,1
$this->assertNotEquals(array(1, 1), $position);
} else {
$this->assertEquals(array(1, 1), $position);
}
}

protected function getOutputContent(StreamOutput $output)
{
rewind($output->getStream());

return str_replace(PHP_EOL, "\n", stream_get_contents($output->getStream()));
}

protected function getOutputStream(): StreamOutput
{
return new StreamOutput($this->stream, StreamOutput::VERBOSITY_NORMAL);
}
}

0 comments on commit 7501b0f

Please sign in to comment.