Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Console] Modify console output and print multiple modifyable sections #24363

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

Closed
wants to merge 3 commits into from

Conversation

pierredup
Copy link
Contributor

@pierredup pierredup commented Sep 28, 2017

Q A
Branch? 4.1
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? TBD
Fixed tickets
License MIT
Doc PR symfony/symfony-docs#9304

Add support to create different output sections for the console output.
Each section is it's own 'stream' of output, where the output can be modified (even if there are other output after it). This allows you to modify previous output in the console, either by appending new lines, modifying previous lines or clearing the output. Modifying a sections output doesn't affect the output after that or in other sections.

Some examples of what can be done:

Overwriting content in a previous section:

Code:

$section1 = $output->section();
$section2 = $output->section();

$section1->writeln("<comment>Doing something</comment>\n");
usleep(500000);
$section2->writeln('<info>Result of first operation</info>');
usleep(500000);

$section1->overwrite("<comment>Doing something else</comment>\n");
usleep(500000);
$section2->writeln('<info>Result of second operation</info>');
usleep(500000);

$section1->overwrite("<comment>Finishing</comment>\n");
usleep(500000);
$section2->writeln('<info>Last Result</info>');

Result:
overwrite-append

Multiple Progress Bars:

Code:

$section1 = $output->section();
$section2 = $output->section();

$progress = new ProgressBar($section1);
$progress2 = new ProgressBar($section2);

$progress->start(100);
$progress2->start(100);

$c = 0;
while (++$c < 100) {
    $progress->advance();

    if ($c % 2 === 0) {
        $progress2->advance(4);
    }

    usleep(500000);
}

Result:
multiple-progress

Modifying content of a table & updating a progress bar:

Code:

$section1 = $output->section();
$section2 = $output->section();

$progress = new ProgressBar($section1);
$table = new Table($section2);

$table->addRow(['Row 1']);
$table->render();

$progress->start(5);

$c = 0;
while (++$c < 5) {
    $table->appendRow(['Row '.($c + 1)]);

    $progress->advance();

    usleep(500000);
}

$progress->finish();
$section1->clear();

Result:
progress-table

Example with Symfony Installer:*

Before:
sf-installer-old

After:
sf-installer

TODO:

  • Add unit tests

@pierredup pierredup changed the title [Console] Add section output to console [Console] Modify console output and print multiple modifyable sections Sep 28, 2017
@javiereguiluz
Copy link
Member

I love this proposal! Just the other day I was playing with Yeoman Generator and they use a similar technique and it amazed me. They make things appear and disappear and change the previous contents, for example when asking several consecutive questions:

yeoman-generator

yeoman-generator-2

stof
stof previously requested changes Sep 29, 2017
$buffer = 0;

foreach ($this->ref as $outputStream) {
if (spl_object_id($outputStream) === spl_object_id($output)) {
Copy link
Member

Choose a reason for hiding this comment

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

just compare the objects themselves inside of comparing the ids explicitly. It will be easier to understand (and will avoid the dependency on the polyfill).

$content = '';

foreach ($this->ref as $outputStream) {
if (spl_object_id($outputStream) === spl_object_id($output)) {
Copy link
Member

Choose a reason for hiding this comment

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

same here

@stof
Copy link
Member

stof commented Sep 29, 2017

this is clearly lacking tests.

@pierredup
Copy link
Contributor Author

@stof The todo at the end of the description says that tests still needs to be added

Copy link
Member

@yceruto yceruto left a comment

Choose a reason for hiding this comment

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

Nice feature, thanks!

* @param resource $stream
* @param OutputSectionReference $sectionReference
* @param int $verbosity
* @param null $decorated
Copy link
Member

Choose a reason for hiding this comment

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

bool|null? null for auto-guessing

* @param OutputSectionReference $sectionReference
* @param int $verbosity
* @param null $decorated
* @param OutputFormatterInterface $formatter
Copy link
Member

Choose a reason for hiding this comment

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

OutputFormatterInterface|null? null to use default OutputFormatter

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See reply below, we want to use the values from the original console output

* @param null $decorated
* @param OutputFormatterInterface $formatter
*/
public function __construct($stream, OutputSectionReference $sectionReference, $verbosity, $decorated, OutputFormatterInterface $formatter)
Copy link
Member

Choose a reason for hiding this comment

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

..., $verbosity = self::VERBOSITY_NORMAL, $decorated = null, OutputFormatterInterface $formatter = null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure if these values should to be defaulted, as we want to inherit all the values from origin console output

Copy link
Member

@yceruto yceruto Sep 29, 2017

Choose a reason for hiding this comment

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

It's pragmatic right now, but out-the-box we would want to use defaults for simplicity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This class should never be used on it's own, it's just an implementation detail and should always be constructed from the ConsoleOutput, which means it should always inherit the values set there (default or otherwise)

Copy link
Member

Choose a reason for hiding this comment

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

That means the PHPDoc of the constructor could be removed altogether if so?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Dockblock removed

*
* @author Pierre du Plessis <[email protected]>
*/
final class OutputSectionReference
Copy link
Member

@yceruto yceruto Sep 29, 2017

Choose a reason for hiding this comment

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

I wondering if we can remove the final constraint and add @final annotation instead?

Copy link
Member

Choose a reason for hiding this comment

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

The annotation would only be useful if we wanted to declare the class final after a stable release. Since it's a new class, the final keyword would be the way to go, imho.

Copy link
Contributor Author

@pierredup pierredup Oct 1, 2017

Choose a reason for hiding this comment

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

@ostrolucky This is a purely internal class, whether it's final or not should not make any difference to the user as they would never know about this class

$prevLines = explode("\n", $messages);

for ($i = 0; $i < count($prevLines); ++$i) {
$total += ceil(strlen($prevLines[$i]) / $this->terminal->getWidth()) ?: 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should use a better function than strlen (only visible characters must be counted)

Copy link
Member

Choose a reason for hiding this comment

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

Helper::strlen() should be used

@fabpot fabpot added this to the 4.1 milestone Sep 29, 2017
@ostrolucky
Copy link
Contributor

ostrolucky commented Oct 1, 2017

Only first example works properly for me. It also works based on a viewport, I don't think that's by purpose

//edit: it has been fixed, removed images as they took too much space

@pierredup
Copy link
Contributor Author

@ostrolucky Can you please try the examples again, there was some missing changes that I forgot to commit

It also works based on a viewport

A limitation with the console is that you can only manipulate anything in the viewport. So if any content stretches outside the viewport, it becomes extremely difficult (if not impossible) to modify the contents.

*/
public function section()
{
return new ConsoleSectionOutput($this->getStream(), $this->outputReference, $this->getVerbosity(), $this->isDecorated(), $this->getFormatter());
Copy link
Member

Choose a reason for hiding this comment

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

This method should throw if !$this->isDecorated()

Copy link
Contributor Author

@pierredup pierredup Oct 1, 2017

Choose a reason for hiding this comment

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

@chalasr I don't think this should throw as it might cause issues in E.G a CI environment where the output should just continue printing without decoration. If this throws an exception, the commands would fail and not continue to run, which might not be the expected behaviour. I have instead added checks for decorated to skip the ANSI codes

$prevLines = explode("\n", $messages);

for ($i = 0; $i < count($prevLines); ++$i) {
$total += ceil(strlen($prevLines[$i]) / $this->terminal->getWidth()) ?: 1;
Copy link
Member

Choose a reason for hiding this comment

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

Helper::strlen() should be used

$prevLines = explode("\n", $messages);

for ($i = 0; $i < count($prevLines); ++$i) {
$total += ceil(Helper::strlen($prevLines[$i]) / $this->terminal->getWidth()) ?: 1;
Copy link
Contributor

@joelwurtz joelwurtz Oct 2, 2017

Choose a reason for hiding this comment

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

I don't think it's still the good way to calculate the length of a line in a console, as an example a tabulation must be count as 8 when displaying (or other number depending on the term configuration) or a colored string / background will have special chars which here will be counted, but they must not.

I was confronted with the same behavior and use this implementation : https://github.com/jolicode/asynit/blob/master/src/Output/TestOutput.php#L110 far from being perfect but gives a good start.

@@ -279,6 +280,11 @@ public function setRow($column, array $row)
*/
public function render()
{
if ($this->output instanceof ConsoleSectionOutput) {
// TODO: Calculate the number of lines to remove for the table
Copy link
Contributor

Choose a reason for hiding this comment

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

outdated todo?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No this still needs to be done, currently it's only calculating the number of rows and headers, but not taking the row separator into account

@ostrolucky
Copy link
Contributor

ostrolucky commented Oct 2, 2017

This kinda changes behaviour of Table helper. I don't think it's acceptable. Table helper without this patch doesn't redraw it's content. Calling

$table->render();
$table->render();

would normally output table twice, but this patch makes it erase first one and replace it with second one

Second thing is that ConsoleSectionOutput always appends newline to output.

Third thing, ProgressBar only works thanks to special patch. Anything other utilizing escape sequences won't work.

Trying to figure out how to make this universal.

@pierredup
Copy link
Contributor Author

pierredup commented Oct 3, 2017

The change in behaviour for the table helper is intented. The table is rendered in a section output, that can be modified. This adds a feature to the table helper where you can modify and re-render the table without rendering it twice. If you want the normal behaviour, just pass the normal console output instance to the table and not a sectioned output.

Second thing is that ConsoleSectionOutput always appends newline to output.

The newline at the end is currently a requirement, as it becomes difficult to navigate the console and change the contents without it. But I am working on a patch.

Anything other utilizing escape sequences won't work.

I'm not sure if we should cater for output with escape sequences. If a user wants to use escape sequences, they should just use the standard console output. If they want to modify the output, then they should use the section output.

This PR is not finished, there are a lot of edge cases and some missing functionality that I'm working on to solve, which I will push once it's done. Thanks for the feedback so far

@chalasr
Copy link
Member

chalasr commented Oct 3, 2017

@ostrolucky If the Table behavior is different only when using the new output class then that means Table just leverages the new feature. If passing the existing outputs gives the same result as before (reading the code I believe it does) then there is no issue.

@ostrolucky
Copy link
Contributor

ostrolucky commented Oct 3, 2017

@chalasr I still don't think this feature should affect behaviour of current components. Purpose of this feature is just ability to split screen to sections. How is forcing table to redraw itself related to that? Here is better example why is this wrong:

$table1->render();
$table2->render();

You would expect both tables to be shown on the screen. Instead, content of second table will overwrite content of first table, unless explicitly assigned different output section. I don't think this is expected. If you do this in a loop or have branching logic which determines if, when and which table is supposed to be outputted, you will quickly find out that this approach is unmaintainable.

Rest of my points are not so important, solving them would require to make implementation a lot more complicated, I was just not sure if they are important for you so wanted to brought them up. If they aren't, they aren't for me either.

@pierredup
Copy link
Contributor Author

@ostrolucky How will the second table overwrite the first one? If you use different output sections, they will still render both, as the table is only redrawn within it's own section. If you use the normal console output and not the new class, both tables will still be shown as it won't use the new functionality. So IMO there is no unexpected behaviour. The only missing part in this instance is that the new class doesn't take into account the output of the normal console. But that is one of the fixes I'm working on. So if you combine thr normal console output and the output of the new class, then you might run into some rendering issue, but that should be fixed soon

@ostrolucky
Copy link
Contributor

You didn't continue to read. I did mention that you if you want to avoid this, you must use different output sections and that's complicated when you have any kind of logic. For example:

$section1 = $output->section();
$section2 = $output->section();

$section2->writeln('some stuff');

while (true) {
    $table = new Table($section1);
    $table->setHeaders(['Stuff']);
    $table->addRow([$rand]);
    $table->render();
}

Now, how do I output contents of these tables above section2 without redrawing them? Now I am forced to manipulate with order of the sections container directly, because I have to create new output sections on the fly and push them in between.

@pierredup
Copy link
Contributor Author

Then just use $table = new Table($output); as you would normally have instead of using $output->section()

@pierredup pierredup force-pushed the console-section-output branch from 2ff25c1 to 00ea1f1 Compare March 20, 2018 07:16
@pierredup pierredup force-pushed the console-section-output branch from 00ea1f1 to e6573b7 Compare March 20, 2018 07:19
@pierredup
Copy link
Contributor Author

pierredup commented Mar 20, 2018

Last comments addressed and table bug fixed. I tested all the original examples and every one now works as expected.

@ostrolucky Thank you for all the effort in properly testing and reviewing this.

Status: Needs Review

Copy link
Member

@chalasr chalasr left a comment

Choose a reason for hiding this comment

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

A new entry in the console CHANGELOG and 👍 for me, great work @pierredup!

@@ -5,6 +5,7 @@ CHANGELOG
-----

* added option to run suggested command if command is not found and only 1 alternative is available
* added option to modify console output and print multiple modifyable sections
Copy link
Member

Choose a reason for hiding this comment

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

modifiable

@@ -252,6 +256,25 @@ public function addRow($row)
return $this;
}

/**
* Add a row to the table, and re-render the table.
Copy link
Member

Choose a reason for hiding this comment

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

Adds
re-renders

}

if ($numberOfLinesToClear > 0) {
// Move cursor up n lines
Copy link
Member

Choose a reason for hiding this comment

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

move

@fabpot
Copy link
Member

fabpot commented Mar 20, 2018

Thank you @pierredup.

@fabpot fabpot closed this Mar 20, 2018
fabpot added a commit that referenced this pull request Mar 20, 2018
…ifyable sections (pierredup)

This PR was squashed before being merged into the 4.1-dev branch (closes #24363).

Discussion
----------

[Console] Modify console output and print multiple modifyable sections

| Q             | A
| ------------- | ---
| Branch?       | 4.1
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | TBD
| Fixed tickets |
| License       | MIT
| Doc PR        | symfony/symfony-docs#9304

Add support to create different output sections for the console output.
Each section is it's own 'stream' of output, where the output can be modified (even if there are other output after it). This allows you to modify previous output in the console, either by appending new lines, modifying previous lines or clearing the output. Modifying a sections output doesn't affect the output after that or in other sections.

Some examples of what can be done:

**Overwriting content in a previous section:**

Code:

```php
$section1 = $output->section();
$section2 = $output->section();

$section1->writeln("<comment>Doing something</comment>\n");
usleep(500000);
$section2->writeln('<info>Result of first operation</info>');
usleep(500000);

$section1->overwrite("<comment>Doing something else</comment>\n");
usleep(500000);
$section2->writeln('<info>Result of second operation</info>');
usleep(500000);

$section1->overwrite("<comment>Finishing</comment>\n");
usleep(500000);
$section2->writeln('<info>Last Result</info>');
```

Result:
![overwrite-append](https://user-images.githubusercontent.com/144858/30975030-769f2c46-a471-11e7-819f-c3698b43f0af.gif)

**Multiple Progress Bars:**

Code:

```php
$section1 = $output->section();
$section2 = $output->section();

$progress = new ProgressBar($section1);
$progress2 = new ProgressBar($section2);

$progress->start(100);
$progress2->start(100);

$c = 0;
while (++$c < 100) {
    $progress->advance();

    if ($c % 2 === 0) {
        $progress2->advance(4);
    }

    usleep(500000);
}
```

Result:
![multiple-progress](https://user-images.githubusercontent.com/144858/30975119-b63222be-a471-11e7-89aa-a555cdf3d2e0.gif)

**Modifying content of a table & updating a progress bar:**

Code:

```php
$section1 = $output->section();
$section2 = $output->section();

$progress = new ProgressBar($section1);
$table = new Table($section2);

$table->addRow(['Row 1']);
$table->render();

$progress->start(5);

$c = 0;
while (++$c < 5) {
    $table->appendRow(['Row '.($c + 1)]);

    $progress->advance();

    usleep(500000);
}

$progress->finish();
$section1->clear();
```

Result:
![progress-table](https://user-images.githubusercontent.com/144858/30975176-e332499c-a471-11e7-9d4f-f58b464a53c2.gif)

**Example with Symfony Installer:***

Before:
![sf-installer-old](https://user-images.githubusercontent.com/144858/30975291-40f22106-a472-11e7-8836-bc39139c2d30.gif)

After:
![sf-installer](https://user-images.githubusercontent.com/144858/30975302-4a00acf4-a472-11e7-83ba-88ea9d0f0f3f.gif)

TODO:
- [x] Add unit tests

Commits
-------

9ec51a1 [Console] Modify console output and print multiple modifyable sections
@pierredup pierredup deleted the console-section-output branch April 3, 2018 20:48
@fabpot fabpot mentioned this pull request May 7, 2018
javiereguiluz added a commit to symfony/symfony-docs that referenced this pull request May 27, 2018
…redup)

This PR was squashed before being merged into the 4.1 branch (closes #9304).

Discussion
----------

Add documentation about manipulating console output

Add documentation about manipulating console output (symfony/symfony#24363)

Commits
-------

3b0e00c Add documentation about manipulating console output
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Console Feature ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" Status: Reviewed
Projects
None yet
Development

Successfully merging this pull request may close these issues.