Slide 1

Slide 1 text

Terminal Fabien Potencier

Slide 2

Slide 2 text

Terminal Fabien Potencier

Slide 3

Slide 3 text

15 years ago... June 14, 2008 symfony 1.1 introduces "tasks"

Slide 4

Slide 4 text

Console is one of the oldest Symfony Component

Slide 5

Slide 5 text

Console is the most used/downloaded Symfony Component

Slide 6

Slide 6 text

Symfony Console > Commands Input: parses CLI arguments/options Code: your own business logic Output: prints, redirect, pipe > Low-level terminal abstractions Cursor Color Style ...

Slide 7

Slide 7 text

namespace Symfony\Component\Console; final class Color { public function __construct(string $foreground = '', string $background = '', array $options = []) bold underscore blink reverse conceal Is it a color? $color = new Color('#000', 'white', ['underscore', 'reverse']); Color Class Console 7.0

Slide 8

Slide 8 text

Introducing the Terminal Component

Slide 9

Slide 9 text

Colors

Slide 10

Slide 10 text

class Color { public function __construct( private readonly string $value, ) { } ANSI-4 16 colors ANSI-256 256 colors ANSI-24 "true colors" Just a color! $color = new Color('#000'); $color = new Color('red'); Color Class Terminal 8.0 $color = $color->tint(20); $color = $color->shade(20); $color = $color->scale(20); $color = $color->mix($anotherColor, 20); Like Saas $context = new Context( colorMode: ColorMode::Ansi8, ); Color "degradation"

Slide 11

Slide 11 text

$color = new AdaptiveColor('#0f0', '#ff0'); $context = new Context( darkMode: true, ); Color adapts depending on your terminal background Light/Dark background? Terminal 8.0 Light Dark

Slide 12

Slide 12 text

$theme = new BootstrapColorTheme(); $color = $theme['primary']; $color = $theme['bg-primary']; // dark mode supported $color = $theme['blue']; $color = $theme['blue-200']; "Semantic" colors ColorTheme > BootstrapColorTheme Terminal 8.0 "Base" colors Dark/Light colors

Slide 13

Slide 13 text

From Colors to Styles namespace Symfony\Component\Console; final class Color { public function __construct(string $foreground = '', string $background = '', array $options = [])

Slide 14

Slide 14 text

Styling, Formatting, ... /** * Formats a message within a section. * * @param string $section The section name * @param string $text The text message * @param integer $size The maximum size allowed for a line (65 by default) */ public function formatSection($section, $text, $size = null) { return sprintf(">> %-$9s %s", $section, $this->excerpt($text, $size)); } /** * Formats a message within a section. */ public function formatSection(string $section, string $message, string $style = 'info'): string { return sprintf('<%s>[%s]%s> %s', $style, $section, $style, $message); } Console 7.0 circa 2023 Tasks 1.1 circa 2008

Slide 15

Slide 15 text

$style = new Style(); echo $style->render('Hello World!')."\n"; echo $style->foreground('#0f0')->render('Hello World!')."\n"; echo $style->background('#0f0')->render('Hello World!')."\n"; echo $style->foreground('#0f0')->background('#0f0')->render('Hello World!')."\n"; echo $style->bold()->render('Hello World!')."\n"; echo $style->italic()->render('Hello World!')."\n"; echo $style->bold()->italic()->render('Hello World!')."\n"; echo $style->link('https://symfony.com')->render('Hello World!')."\n"; $style = (new Style()) ->foreground('#0f0') ->background('#0f0') ->bold() ->italic() ; Rendering a Style Style Terminal 8.0 Colors "Options" like bold/italic Reuse styles

Slide 16

Slide 16 text

echo $style ->foreground($theme['indigo']) ->background($theme['yellow']) ->render('Hello World!')."\n" ; Inline vs Block Terminal 8.0 inline by default echo $style ->display(Style::DISPLAY_BLOCK) ->render('Hello World!') ; block display echo $style ->foreground($theme['indigo']) ->background($theme['yellow']) ->render('Hello World!')."\n" ; Style with a ColorTheme

Slide 17

Slide 17 text

echo $style ->display(Style::DISPLAY_BLOCK) ->foreground($theme['yellow-300']) ->background($theme['pink-600']) ->height(10) ->align(Position::MIDDLE) ->render('Hello World!') ; echo $style ->display(Style::DISPLAY_BLOCK) ->foreground($theme['yellow-300']) ->background($theme['pink-600']) ->width(40) ->align(Position::CENTER) ->render('Hello World!') ; Size & Alignement Terminal 8.0

Slide 18

Slide 18 text

echo $style ->display(Style::DISPLAY_BLOCK) ->foreground($theme['yellow-300']) ->background($theme['pink-600']) ->width(40) ->height(10) ->align(Position::MIDDLE | Position::CENTER) ->render('Hello World!') ; Size & Alignement Terminal 8.0

Slide 19

Slide 19 text

CSS on the console?

Slide 20

Slide 20 text

echo $style ->display(Style::DISPLAY_BLOCK) ->padding(4, 6) ->foreground($theme['yellow-300']) ->background($theme['pink-600']) ->align(Position::MIDDLE | Position::CENTER) ->render('Hello World!') ; Padding Terminal 8.0 padding(4) padding(4, 6) padding(4, 7, 2, 6)

Slide 21

Slide 21 text

echo $style ->display(Style::DISPLAY_BLOCK) ->foreground($theme['yellow-300']) ->background($theme['pink-600']) ->align(Position::MIDDLE | Position::CENTER) ->padding(4, 6) ->margin(2, 4) ->render('Hello World!') ; Margin Terminal 8.0 margin(4) margin(4, 6) margin(4, 6, 4, 6)

Slide 22

Slide 22 text

echo $style ->display(Style::DISPLAY_BLOCK) ->foreground($theme['yellow-300']) ->background($theme['pink-600']) ->align(Position::MIDDLE | Position::CENTER) ->padding(4, 6) ->margin(2, 4) ->borderStyle(BorderStyle::ROUNDED) ->render('Hello World!') ; Border Terminal 8.0

Slide 23

Slide 23 text

$borderStyle = $style ->display(Style::DISPLAY_BLOCK) ->padding(0, 1) ->marginBottom(1) ->background($theme['yellow']) ->foreground($theme['indigo']) ->borderBackground($theme['yellow']) ->borderForeground($theme['indigo']) ->align(Position::MIDDLE | Position::CENTER) ; Let's reuse the base style Terminal 8.0

Slide 24

Slide 24 text

echo $borderStyle ->borderStyle(BorderStyle::WIDE) ->render('Hello World!') ; echo $borderStyle ->borderStyle(BorderStyle::TALL) ->render('Hello World!') ; Smart Borders Terminal 8.0

Slide 25

Slide 25 text

Smart Borders Terminal 8.0 ['▁', '▁', '▁'] ['▎', ' ', '▊'] ['▔', '▔', '▔'] ['▊', '▔', '▎'] ['▊', ' ', '▎'] ['▊', '▁', '▎'] [2, 0, 1] [2, 0, 1] [2, 0, 1] [1, 1, 1] [0, 1, 3] [1, 1, 1]

Slide 26

Slide 26 text

$titleStyle = $style ->display(Style::DISPLAY_BLOCK) ->padding(0, 2) ->foreground($theme['indigo']) ->align(Position::MIDDLE | Position::CENTER) ->borderStyle(BorderStyle::WIDE) ->borderBottom(true) ; echo $titleStyle->render('Hello World!'); Be creative with borders Terminal 8.0

Slide 27

Slide 27 text

echo $titleStyle->toString(); Debugging styles Terminal 8.0

Slide 28

Slide 28 text

printf('%-10s ', 'Shade'); for ($i = 1; $i <= 9; $i++) { echo $style->foreground($theme['indigo-'.(100 * $i)])->render('▃▃'); } printf('%-10s ', 'Scale'); for ($i = -$max; $i <= $max; $i++) { echo $style->foreground($theme['indigo']->scale((int) (- $i / $max * 100)))->render('▃'); } printf('%-10s ', 'Shade'); for ($i = 0; $i <= $max; $i++) { echo $style->foreground($theme['indigo']->shade((int) (100 - $i / $max * 100)))->render('▃'); } printf('%-10s ', 'Tint'); for ($i = 0; $i <= $max; $i++) { echo $style->foreground($theme['indigo']->tint((int) (100 - $i / $max * 100)))->render('▃'); } printf('%-10s ', 'Mix'); for ($i = 0; $i <= $max; $i++) { echo $style->foreground($theme['indigo']->mix($theme['yellow'], (int) (100 - $i / $max * 100)))->render('▃'); } Terminal 8.0 foreach ($theme::BASE_SEMANTIC_COLORS as $name) { printf('%-10s ', ucfirst($name)); for ($i = 1; $i <= 9; $i++) { print($style->foreground($theme[$name.'-'.($i * 100)]) ->render('██')); } } printf('%-10s ', 'Gray'); for ($i = 1; $i <= 9; $i++) { print($style->foreground($theme['gray-'.($i * 100)]) ->render('██')); }

Slide 29

Slide 29 text

From a Style to a Stylesheet

Slide 30

Slide 30 text

$stylesheet = new DefaultStylesheet(); echo $stylesheet['span']->render('Hello World!')."\n"; echo $stylesheet['div']->render('Hello World!'); echo $stylesheet['div']->bold()->render('Hello World!'); echo $stylesheet['b']->render('Hello World!')."\n"; echo $stylesheet['div bold']->render('Hello World!'); echo $stylesheet['h1']->render('Hello World!'); echo $stylesheet['h2']->render('Hello World!'); Stylesheet Terminal 8.0

Slide 31

Slide 31 text

echo $stylesheet['p p-2 my-1 italic text-html-silver bg-html-green'] ->render('Hello World!'); Tailwind-like Terminal 8.0

Slide 32

Slide 32 text

$stylesheet['my-box'] = $stylesheet['p p-2 italic text-html-silver bg-html-green']; echo $stylesheet['my-box w-full']->render('Hello World!')."\n"; Compose styles Terminal 8.0

Slide 33

Slide 33 text

$stylesheet = new DefaultStylesheet(); foreach (ColorTheme::BASE_SEMANTIC_COLORS as $name) { echo $stylesheet['.alert-'.$name]->render('This is a '.$name.' alert box!'); } $content = $stylesheet['h1']->render('Well done!'). $stylesheet['p']->render('Lorem ipsum...'). $stylesheet['p']->marginBottom(0)->render('Lorem ipsum...') ; echo $stylesheet['.alert-success']->render($content); Default useful styles like Bootstrap alerts Terminal 8.0

Slide 34

Slide 34 text

$term = new Terminal(); $stdout = $term->stdout(); dump( $stdout->hasDarkBackground(), $stdout->getWidth(), $stdout->getHeight(), ); print "\n\n"; $context = $stdout->getContext(); dump($context); print "\n\n"; $cursor = $stdout->cursor() $cursor->moveToPosition(10, 10); // operations on the screen $screen = $stdout->screen(); $screen->enableMouse(); $screen->clear(); Terminal Terminal 8.0

Slide 35

Slide 35 text

Organizing content

Slide 36

Slide 36 text

$screen = new Columns( 'LEFT', 'RIGHT', ); echo $screen->render(); $stylesheet['.screen'] = (new Style()) ->width('terminal["width"]') ->height(20) ; $screen = (new Columns( new StyledContent('LEFT', $stylesheet['.cell w-2fr']), new StyledContent('RIGHT', $stylesheet['.cell w-1fr']), ))->style($stylesheet['.screen']); echo $screen->render(); $screen = (new Rows( $screen, $screen, ))->style($stylesheet['.screen']); echo $screen->render(); Columns and Rows Terminal 8.0 2fr 1fr $stylesheet['.cell'] = (new Style()) ->borderStyle(BorderStyle::ROUNDED) ->align(Position::CENTER) ->borderForeground('color("border-primary")') ;

Slide 37

Slide 37 text

$screen = (new Columns( (new Rows( new StyledContent('UP', $stylesheet['.cell']), new StyledContent('MIDDLE', $stylesheet['.cell']), new StyledContent(' LOW ', $stylesheet['.cell']), ))->position(Position::MIDDLE)->style($stylesheet['.cell w-1fr']), new StyledContent('CENTER', $stylesheet['.cell w-1fr']), new StyledContent(trim(str_repeat("RIGHT\n", 15)), $stylesheet['.cell w-1fr']), ))->position(Position::BOTTOM)->style($stylesheet['.screen']); echo $screen->render(); Columns and Rows Terminal 8.0

Slide 38

Slide 38 text

From Static to Interactive with Widgets

Slide 39

Slide 39 text

interface WidgetInterface extends RenderInterface { public function init(Stylesheet $stylesheet, LoggerInterface $logger): void; public function update(MessageInterface $message): ?MessageInterface; } interface RenderInterface { public function render(): string; } WidgetInterface Terminal 8.0 ExitMessage KeyMessage MouseMessage TickMessage WindowSizeMessage

Slide 40

Slide 40 text

class HelloWorldWidget implements WidgetInterface { public function init(Stylesheet $stylesheet, LoggerInterface $logger): void { } public function update(MessageInterface $message): ?MessageInterface { if ($message instanceof KeyMessage && 'q' === (string) $message->key) { return new ExitMessage(); } return null; } public function render(): string { return 'Hello World!'; } } $term = new Terminal(); $stylesheet = new DefaultStylesheet(); $app = new Application($term, $stylesheet, new HelloWorldWidget()); $app->run(); HelloWorldWidget Terminal 8.0 Render Update the state Infinite loop

Slide 41

Slide 41 text

A Pager Terminal 8.0 Supported keys up and down page up and down go to top go to bottom

Slide 42

Slide 42 text

public function __construct(string $content) { $this->viewport = new ViewportWidget(); $this->viewport->setContent($content, substr_count($content, "\n")); } public function init(Stylesheet $stylesheet, LoggerInterface $logger): void { $this->stylesheet = $stylesheet; $this->viewport->init($stylesheet, $logger); } public function update(MessageInterface $message): ?MessageInterface { $this->viewport->update($message); return null; } public function render(): string { return $this->stylesheet['span p-1 border-double'] ->render($this->viewport->render()); } A Pager Terminal 8.0 Update the Viewport Propagate init() Render the Viewport Rely on another widget

Slide 43

Slide 43 text

$screen->enableMouse(); try { $app->run(); } finally { $screen->disableMouse(); } Mouse support Terminal 8.0 if ($message instanceof WindowSizeMessage) { $this->viewport->width($message->width); $this->viewport->height($message->height); } Window resizing support

Slide 44

Slide 44 text

Demo Time? Terminal

Slide 45

Slide 45 text

A component with many sub-parts Multi-year effort - release gradually 7.1 -> 8.0 Progressively use it internally in Console Help from the community needed Terminal

Slide 46

Slide 46 text

First Pull request: Empty "shell" Terminal 8.0 Second one: Color sub-namespace

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

https://symfony.com/sponsor Sponsor Symfony Thank you!