Skip to content

Commit 80d59d5

Browse files
pierredupfabpot
authored andcommitted
[Console] Add Cursor class to control the cursor in the terminal
1 parent 4dabd00 commit 80d59d5

File tree

6 files changed

+361
-14
lines changed

6 files changed

+361
-14
lines changed

src/Symfony/Component/Console/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* `Command::setHidden()` is final since Symfony 5.1
88
* Add `SingleCommandApplication`
9+
* Add `Cursor` class
910

1011
5.0.0
1112
-----
+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console;
13+
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
16+
/**
17+
* @author Pierre du Plessis <pdples@gmail.com>
18+
*/
19+
class Cursor
20+
{
21+
private $output;
22+
23+
private $input;
24+
25+
public function __construct(OutputInterface $output, $input = STDIN)
26+
{
27+
$this->output = $output;
28+
$this->input = $input;
29+
}
30+
31+
public function moveUp(int $lines = 1)
32+
{
33+
$this->output->write(sprintf("\x1b[%dA", $lines));
34+
}
35+
36+
public function moveDown(int $lines = 1)
37+
{
38+
$this->output->write(sprintf("\x1b[%dB", $lines));
39+
}
40+
41+
public function moveRight(int $columns = 1)
42+
{
43+
$this->output->write(sprintf("\x1b[%dC", $columns));
44+
}
45+
46+
public function moveLeft(int $columns = 1)
47+
{
48+
$this->output->write(sprintf("\x1b[%dD", $columns));
49+
}
50+
51+
public function moveToColumn(int $column)
52+
{
53+
$this->output->write(sprintf("\x1b[%dG", $column));
54+
}
55+
56+
public function moveToPosition(int $column, int $row)
57+
{
58+
$this->output->write(sprintf("\x1b[%d;%dH", $row + 1, $column));
59+
}
60+
61+
public function savePosition()
62+
{
63+
$this->output->write("\x1b7");
64+
}
65+
66+
public function restorePosition()
67+
{
68+
$this->output->write("\x1b8");
69+
}
70+
71+
public function hide()
72+
{
73+
$this->output->write("\x1b[?25l");
74+
}
75+
76+
public function show()
77+
{
78+
$this->output->write("\x1b[?25h\x1b[?0c");
79+
}
80+
81+
/**
82+
* Clears all the output from the current line.
83+
*/
84+
public function clearLine(bool $fromCurrentPosition = false)
85+
{
86+
if (true === $fromCurrentPosition) {
87+
$this->output->write("\x1b[K");
88+
} else {
89+
$this->output->write("\x1b[2K");
90+
}
91+
}
92+
93+
/**
94+
* Clears all the output from the cursors' current position to the end of the screen.
95+
*/
96+
public function clearOutput()
97+
{
98+
$this->output->write("\x1b[0J", false);
99+
}
100+
101+
/**
102+
* Clears the entire screen.
103+
*/
104+
public function clearScreen()
105+
{
106+
$this->output->write("\x1b[2J", false);
107+
}
108+
109+
/**
110+
* Returns the current cursor position as x,y coordinates.
111+
*/
112+
public function getCurrentPosition(): array
113+
{
114+
static $isTtySupported;
115+
116+
if (null === $isTtySupported && \function_exists('proc_open')) {
117+
$isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes);
118+
}
119+
120+
if (!$isTtySupported) {
121+
return [1, 1];
122+
}
123+
124+
$sttyMode = shell_exec('stty -g');
125+
shell_exec('stty -icanon -echo');
126+
127+
@fwrite($this->input, "\033[6n");
128+
129+
$code = trim(fread($this->input, 1024));
130+
131+
shell_exec(sprintf('stty %s', $sttyMode));
132+
133+
sscanf($code, "\033[%d;%dR", $row, $col);
134+
135+
return [$col, $row];
136+
}
137+
}

src/Symfony/Component/Console/Helper/ProgressBar.php

+6-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Console\Helper;
1313

14+
use Symfony\Component\Console\Cursor;
1415
use Symfony\Component\Console\Exception\LogicException;
1516
use Symfony\Component\Console\Output\ConsoleOutputInterface;
1617
use Symfony\Component\Console\Output\ConsoleSectionOutput;
@@ -47,6 +48,7 @@ final class ProgressBar
4748
private $overwrite = true;
4849
private $terminal;
4950
private $previousMessage;
51+
private $cursor;
5052

5153
private static $formatters;
5254
private static $formats;
@@ -78,6 +80,7 @@ public function __construct(OutputInterface $output, int $max = 0, float $minSec
7880
}
7981

8082
$this->startTime = time();
83+
$this->cursor = new Cursor($output);
8184
}
8285

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

470-
// Move the cursor to the beginning of the line and erase the line
471-
$message = "\x0D\x1B[2K$message";
472+
$this->cursor->moveToColumn(1);
473+
$this->cursor->clearLine();
472474
}
473475
}
474476
} elseif ($this->step > 0) {

src/Symfony/Component/Console/Helper/QuestionHelper.php

+7-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Console\Helper;
1313

14+
use Symfony\Component\Console\Cursor;
1415
use Symfony\Component\Console\Exception\MissingInputException;
1516
use Symfony\Component\Console\Exception\RuntimeException;
1617
use Symfony\Component\Console\Formatter\OutputFormatter;
@@ -235,6 +236,8 @@ protected function writeError(OutputInterface $output, \Exception $error)
235236
*/
236237
private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string
237238
{
239+
$cursor = new Cursor($output, $inputStream);
240+
238241
$fullChoice = '';
239242
$ret = '';
240243

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

268270
$fullChoice = self::substr($fullChoice, 0, $i);
269271
}
@@ -351,17 +353,14 @@ function ($match) use ($ret) {
351353
}
352354
}
353355

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

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

0 commit comments

Comments
 (0)