Renderer;
use Symfony\Component\Tui\Style\Color;
use Symfony\Component\Tui\Terminal\ScreenBuffer;

class ScreenBufferHtmlRendererTest extends TestCase
{
    /**
     * @param string[] $expectedCss
     */
    #[DataProvider('ansiToHtmlProvider')]
    public function testAnsiToHtmlConversion(string $ansi, array $expectedCss)
    {
        $html = $this->convert($ansi);

        foreach ($expectedCss as $css) {
            $this->assertStringContainsString($css, $html);
        }
    }

    /**
     * @return

=> ["\x1b[1;32mHello\x1b[0m", ['font-weight: bold', 'color: #00cd00']];
        yield 'background color' => ["\x1b[41mHello\x1b[0m", ['background-color: #cd0000']];
        yield 'bright foreground' => ["\x1b[91mHello\x1b[0m", ['color: #ff0000']];

 iterable<string, array{string, string[]}>
     */
    public static function ansiToHtmlProvider(): iterable
    {
        yield 'foreground color' => ["\x1b[32mHello\x1b[0m", ['color: #00cd00']];
        yield 'bold with color'


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests\Focus;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Event\FocusEvent;
use Symfony\Component\Tui\Input\Key;
use Symfony\Component\Tui\Input\Keybindings;
use Symfony\Component\Tui\Terminal\VirtualTerminal;
use Symfony\Component\Tui\Tests\KeySequenceParser;
use Symfony\Component\Tui\Tui;
use Symfony\Component\Tui\Widget\Input

new InputWidget();
        $second = new InputWidget();

        $focusManager->add($first)->add($second);

        $tui->setFocus($first);
        $this->assertSame($first, $tui->getFocus());

        $focusManager->focusNext();
        $this->assertSame(

Widget;

class FocusManagerTest extends TestCase
{
    public function testFocusNavigation()
    {
        $tui = new Tui(terminal: new VirtualTerminal(80, 24));
        $focusManager = $tui->getFocusManager();

        $first =


gumentException;
use Symfony\Component\Tui\Render\CellBuffer;

class CellBufferTest extends TestCase
{
    /**
     * @return iterable<string, array{int, int, string}>
     */
    public static function invalidDimensionsProvider(): iterable
    {
        yield 'zero width' => [0, 5, 'CellBuffer dimensions must be at least 1x1, got 0x5'];
        yield 'zero height' => [10, 0, 'CellBuffer dimensions must be at least 1x1, got 10x0'];
        yield 'negative width' => [-1, 5, 'CellBuffer dimensions must be at

[DataProvider('invalidDimensionsProvider')]
    public function testConstructorRejectsInvalidDimensions(int $width, int $height, string $expectedMessage)
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionM

least 1x1, got -1x5'];
        yield 'negative height' => [10, -3, 'CellBuffer dimensions must be at least 1x1, got 10x-3'];
        yield 'both zero' => [0, 0, 'CellBuffer dimensions must be at least 1x1, got 0x0'];
    }

    #


fony\Component\Tui\Style\Border;
use Symfony\Component\Tui\Style\Direction;
use Symfony\Component\Tui\Style\Padding;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Widget\ContainerWidget;
use Symfony\Component\Tui\Widget\TextWidget;

class LayoutEngineTest extends TestCase
{
    public function testLayoutVerticalSingleChild()
    {
        $renderer = new Renderer();
        $root = new ContainerWidget();
        $root->add(new TextWidget('Hello'));

        $result = $renderer->render($ro

   $root = new ContainerWidget();
        $root->add(new TextWidget('First'));
        $root->add(new TextWidget('Second'));

        $result = $renderer->render($root, 80, 24);

        $this->assertCount(2, $result);
        $this->assertStringContainsSt

ot, 80, 24);

        $this->assertCount(1, $result);
        $this->assertStringContainsString('Hello', $result[0]);
    }

    public function testLayoutVerticalMultipleChildren()
    {
        $renderer = new Renderer();


ertStringContainsString('After', $after[0]);
    }

    public function testRenderCacheInvalidatedWhenDimensionsChange()
    {
        $renderer = new Renderer();
        $fill = new ContainerWidget();
        $fill->expandVertically(true);
        $fill->add(new TextWidget('A'));

        $root = new ContainerWidget();
        $root->add($fill);

        $small = $renderer->render($root, 40, 5);
        $large = $renderer->render($root, 40, 10);

        // The fill child expands differently with different

= new Renderer();
        $root = new ContainerWidget();
        $root->add(new TextWidget('First'));

        $oneChild = $renderer->render($root, 40, 10);

        $root->add(new TextWidget('Second'));
        $twoChildren = $renderer->render($root, 40,

 rows
        $this->assertNotSame($small, $large);
        $this->assertCount(5, $small);
        $this->assertCount(10, $large);
    }

    public function testRenderCacheInvalidatedWhenChildrenChange()
    {
        $renderer


buted with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Widget\BracketedPasteTrait;

class BracketedPasteHandler
{
    use BracketedPasteTrait {
        processBracketedPaste as public;
        isBufferingPaste as public;
    }
}

class BracketedPasteTraitTest extends TestCase
{
    public function testSingleChunkPaste()
    {
        $handler = $this->createHandler();

        $data = "\x1b[200~hello world\x1b[201~";

tMultiChunkPaste()
    {
        $handler = $this->createHandler();

        // First chunk: start marker + partial content
        $data = "\x1b[200~hello ";
        $result = $handler->processBracketedPaste($data);
        $this->assertNull($result);

 $result = $handler->processBracketedPaste($data);

        $this->assertSame('hello world', $result);
        $this->assertSame('', $data);
        $this->assertFalse($handler->isBufferingPaste());
    }

    public function tes


 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\Tui\Widget\Markdown;

use Tempest\Highlight\TerminalTheme;
use Tempest\Highlight\Themes\EscapesTerminalTheme;
use Tempest\Highlight\Tokens\TokenType;
use Tempest\Highlight\Tokens\TokenTypeEnum;

/**
 * Dark terminal theme for syntax highlighting.
 *
 * @experimental
 *

gb = match ($tokenType) {
            TokenTypeEnum::KEYWORD => [255, 122, 178],   // #ff7ab2
            TokenTypeEnum::TYPE => [172, 242, 228],      // #acf2e4
            TokenTypeEnum::PROPERTY => [120, 199, 255],  // #78c7ff (variable)
            Tok

* @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class DarkTerminalTheme implements TerminalTheme
{
    use EscapesTerminalTheme;

    public function before(TokenType $tokenType): string
    {
        $r


d: int, unchanged: int}
     * }
     */
    public static function compare(string $expected, string $actual): array
    {
        if ($expected === $actual) {
            return [
                'identical' => true,
                'summary' => 'Outputs are identical',
                'diff_lines' => [],
                'diff_text' => '',
                'stats' => ['added' => 0, 'removed' => 0, 'changed' => 0, 'unchanged' => 0],
            ];
        }

        $expectedLines = explode("\n", $expected);

y' => \sprintf(
                '%d added, %d removed, %d changed, %d unchanged',
                $stats['added'],
                $stats['removed'],
                $stats['changed'],
                $stats['unchanged']
            ),
            'diff_li

        $actualLines = explode("\n", $actual);

        $diff = self::computeDiff($expectedLines, $actualLines);
        $stats = self::computeStats($diff);

        return [
            'identical' => false,
            'summar


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests\Render;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Render\PositionTracker;
use Symfony\Component\Tui\Render\WidgetRect;
use Symfony\Component\Tui\Widget\TextWidget;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class PositionTrackerTest extends TestCase
{
    // -----------------------------------------

acker();
        $widget = new TextWidget('hello');

        $this->assertNull($tracker->getWidgetRect($widget));
    }

    public function testSetAndGetWidgetRect()
    {
        $tracker = new PositionTracker();
        $widget = new TextWidget('hello')

----------------------
    // Widget position tracking
    // ---------------------------------------------------------------

    public function testGetWidgetRectReturnsNullForUntracked()
    {
        $tracker = new PositionTr


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Event\CancelEvent;
use Symfony\Component\Tui\Terminal\VirtualTerminal;
use Symfony\Component\Tui\Tui;
use Symfony\Component\Tui\Widget\CancellableLoaderWidget;

class CancellableLoaderWidgetTest extends TestCase
{
    public function testCancelViaEscapeKey()
    {

assertSame($loader, $event->getTarget());
        });

        // Escape key = \x1b
        $loader->handleInput("\x1b");

        $this->assertTrue($loader->isCancelled());
        $this->assertTrue($cancelCalled);
    }

    public function testCancelVia

  [$loader, $tui] = $this->createLoaderWithTui();
        $cancelCalled = false;

        $tui->addListener(function (CancelEvent $event) use (&$cancelCalled, $loader): void {
            $cancelCalled = true;
            $this->


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Render\Renderer;
use Symfony\Component\Tui\Style\Color;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Style\StyleSheet;
use Symfony\Component\Tui\Widget\TextWidget;

/**
 * Tests for cascading stylesheets integration with Tui.
 *
 * @author Fabien Potencier <

        $stylesheet = $renderer->getStyleSheet();

        // Default stylesheet should resolve a basic widget
        $widget = new TextWidget('test');
        $style = $stylesheet->resolve($widget);
        // Style should have some defaults from the st

fabien@symfony.com>
 */
class TuiCascadingStylesheetTest extends TestCase
{
    public function testWithoutUserStylesheet()
    {
        // When no stylesheets are provided, defaults are used
        $renderer = new Renderer();


 $stylesheet = new StyleSheet();
        $widget = new TextWidget('Hello');

        $resolved = $stylesheet->resolve($widget);

        $this->assertNull($resolved->getPadding());
        $this->assertNull($resolved->getBorder());
        $this->assertNull($resolved->getBackground());
        $this->assertNull($resolved->getColor());
    }

    public function testResolveWithUniversalSelector()
    {
        $stylesheet = new StyleSheet()
            ->addRule('*', Style::padding([1, 2]));
        $widget

testResolveWithFqcnSelector()
    {
        $stylesheet = new StyleSheet()
            ->addRule(TextWidget::class, new Style()->withColor('red'));
        $widget = new TextWidget('Hello');

        $resolved = $stylesheet->resolve($widget);

        $thi

= new TextWidget('Hello');

        $resolved = $stylesheet->resolve($widget);

        $this->assertSame(1, $resolved->getPadding()->top);
        $this->assertSame(2, $resolved->getPadding()->right);
    }

    public function


(string $input, int $expected)
    {
        $this->assertSame($expected, AnsiUtils::visibleWidth($input));
    }

    public function testVisibleWidthWithAnsiCodes()
    {
        // Red "Hello" with reset
        $this->assertSame(5, AnsiUtils::visibleWidth("\x1b[31mHello\x1b[0m"));

        // Bold + colors
        $this->assertSame(5, AnsiUtils::visibleWidth("\x1b[1;31mHello\x1b[0m"));
    }

    public function testVisibleWidthWithEmoji()
    {
        // Most emojis are 2 columns wide
        $this->a

>assertSame(2, AnsiUtils::visibleWidth('日'));
        $this->assertSame(4, AnsiUtils::visibleWidth('日本'));
    }

    public function testVisibleWidthConsistencyBetweenSlowPathAndGraphemeWidth()
    {
        // Ensure visibleWidth (which uses mb_strwidth

ssertSame(2, AnsiUtils::visibleWidth('😀'));
        $this->assertSame(4, AnsiUtils::visibleWidth('😀😀'));
    }

    public function testVisibleWidthWithWideChars()
    {
        // CJK characters are 2 columns wide
        $this-


buted with this source code.
 */

namespace Symfony\Component\Tui\Tests\Render;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Ansi\AnsiUtils;
use Symfony\Component\Tui\Render\Renderer;
use Symfony\Component\Tui\Style\Align;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Style\StyleSheet;
use Symfony\Component\Tui\Style\TailwindStylesheet;
use Symfony\Component\Tui\Style\VerticalAlign;
use Symfony\Component\Tui\Widget\ContainerWidg

StyleSheet([
            '.parent' => new Style(align: Align::Center),
            '.child' => new Style(maxColumns: 10),
        ]));

        $root = new ContainerWidget();
        $root->addStyleClass('parent');
        $child = new TextWidget('Hello');

et;
use Symfony\Component\Tui\Widget\TextWidget;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class AlignTest extends TestCase
{
    public function testAlignCenter()
    {
        $renderer = new Renderer(new


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget\Editor;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Widget\Editor\EditorViewport;

class EditorViewportTest extends TestCase
{
    public function testComputeViewportKeepsCursorVisible()
    {
        $viewport = new EditorViewport();
        $lines = [];
        for ($i = 0; $i < 50; ++$i) {
            $lines[] =

   $this->assertLessThan($result['scroll_offset'] + $result['visible_line_count'], 20);
    }

    public function testComputeViewportScrollsUpWhenCursorAbove()
    {
        $viewport = new EditorViewport();
        $lines = [];
        for ($i = 0; $i <

 "Line $i";
        }

        // Cursor at line 20, viewport shows 10 rows
        $result = $viewport->computeViewport($lines, 20, 10, 80, false, 1);

        $this->assertGreaterThanOrEqual($result['scroll_offset'], 20);


buted with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget\Figlet;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Widget\Figlet\FigletFont;
use Symfony\Component\Tui\Widget\Figlet\FigletRenderer;

class FigletRendererTest extends TestCase
{
    private const FONTS_DIR = __DIR__.'/../../../Widget/Figlet/fonts';

    public function testRenderEmptyString()
    {
        $font = FigletFont::load(self::FONTS_DIR.'/big.flf');
        $renderer = new FigletRenderer($font);

nes = $renderer->render('A');

        // big font is 8 tall, trailing blank lines stripped; should be ≤ 8 and ≥ 1
        $this->assertGreaterThanOrEqual(1, \count($lines));
        $this->assertLessThanOrEqual(8, \count($lines));

        // Should conta

 $this->assertSame([], $renderer->render(''));
    }

    public function testRenderSingleCharacter()
    {
        $font = FigletFont::load(self::FONTS_DIR.'/big.flf');
        $renderer = new FigletRenderer($font);

        $li


hp

/*
 * 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\Tui\Tests\Event;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Event\ChangeEvent;
use Symfony\Component\Tui\Widget\AbstractWidget;

class ChangeEventTest extends TestCase
{
    #[DataProvid

());
    }

    public static function isBlankProvider(): iterable
    {
        yield 'empty string' => ['', true];
        yield 'whitespace only' => ['   ', true];
        yield 'tabs and spaces' => ["\t \n", true];
        yield 'non-empty value' => ['

er('isBlankProvider')]
    public function testIsBlank(string $value, bool $expected)
    {
        $event = new ChangeEvent($this->createStub(AbstractWidget::class), $value);

        $this->assertSame($expected, $event->isBlank


hp

/*
 * 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\Tui\Tests\Event;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Event\SubmitEvent;
use Symfony\Component\Tui\Widget\AbstractWidget;

class SubmitEventTest extends TestCase
{
    #[DataProvid

er('isBlankProvider')]
    public function testIsBlank(string $value, bool $expected)
    {
        $event = new SubmitEvent($this->createStub(AbstractWidget::class), $value);

        $this->assertSame($expected, $event->isBlank


class StdinBufferTest extends TestCase
{
    /**
     * @param string[] $expectedSequences
     */
    #[DataProvider('sequenceProvider')]
    public function testProcess(string $input, array $expectedSequences)
    {
        $buffer = new StdinBuffer();
        $sequences = [];

        $buffer->onData(static function (string $data) use (&$sequences) {
            $sequences[] = bin2hex($data);
        });

        $buffer->process($input);

        $this->assertSame($expectedSequences, $sequences);
    }

complete, waiting for more
            ],
            'double escape' => [
                "\x1b\x1b",
                [], // Need to wait for third char to decide
            ],
            'escape then down arrow' => [
                "\x1b\x1b[B",

    /**
     * @return array<string, array{string, string[]}>
     */
    public static function sequenceProvider(): array
    {
        return [
            'single escape' => [
                "\x1b",
                [], // In


 }

    /**
     * @return iterable<string, array{int, int, string, string}>
     */
    public static function writeAndGetScreenProvider(): iterable
    {
        yield 'simple text' => [40, 10, 'Hello, World!', 'Hello, World!'];
        yield 'newlines' => [40, 10, "Line 1\nLine 2\nLine 3", "Line 1\nLine 2\nLine 3"];
        yield 'carriage return' => [40, 10, "First\rSecond", 'Second'];
        yield 'clear screen' => [40, 10, "Old content\x1b[2J\x1b[HNew content", 'New content'];
        yield 'UTF-8 ch

e' => [20, 3, "A\nB\nC\nD\nE\nF", "D\nE\nF"];
        yield 'wide character plain text' => [40, 10, '你好世界', '你好世界'];
        yield 'wide character multiline' => [40, 10, "你好\n世界", "你好\n世界"];
    }

    public function testCursorMovement()
    {
        $sc

aracters' => [40, 10, "→ Option 1\n  Option 2\n✓ Selected", "→ Option 1\n  Option 2\n✓ Selected"];
        yield 'scroll up' => [20, 3, "Line 1\nLine 2\nLine 3\nLine 4", "Line 2\nLine 3\nLine 4"];
        yield 'scroll up multipl


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests\Terminal;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Terminal\VirtualTerminal;

class VirtualTerminalTest extends TestCase
{
    public function testSimulateInputForwardsKeySequences()
    {
        $terminal = new VirtualTerminal();
        $received = [];

        $termina

this->assertSame(['a', 'b', 'c'], $received);

        $terminal->stop();
    }

    public function testSimulateInputForwardsPasteContent()
    {
        $terminal = new VirtualTerminal();
        $received = [];

        $terminal->start(
            sta

l->start(
            static function (string $data) use (&$received) { $received[] = $data; },
            static function () {},
            static function () {},
        );

        $terminal->simulateInput('abc');

        $


 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\Tui\Tests\Loop;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Exception\InvalidArgumentException;
use Symfony\Component\Tui\Loop\FixedStepAccumulator;

class FixedStepAccumulatorTest extends TestCase
{
    public function testConstructorRejectsInvalidArgumen

Accumulator(60.0, 5);

        $this->assertSame(0, $accumulator->computeSteps(1.0 / 120.0));
        $this->assertSame(1, $accumulator->computeSteps(1.0 / 120.0));
    }

    public function testComputeStepsCapsLargeDelta()
    {
        $accumulator = ne

ts()
    {
        $this->expectException(InvalidArgumentException::class);
        new FixedStepAccumulator(0.0);
    }

    public function testComputeStepsAccumulatesFractionalDelta()
    {
        $accumulator = new FixedStep


ge.
 *
 * (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\Tui\Tests;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

/**
 * Tests for KeySequenceParser which converts human-readable key notation
 * to terminal escape sequences.
 */
class KeySequenceParserTest extends TestCase
{
    #[DataProvider('parseKeysProvider')]

lic static function parseKeysProvider(): array
    {
        return [
            'enter' => ['<Enter>', "\r"],
            'return' => ['<Return>', "\r"],
            'escape' => ['<Escape>', "\x1b"],
            'esc' => ['<Esc>', "\x1b"],
            't

  public function testParseKeys(string $input, string $expected)
    {
        $this->assertSame($expected, KeySequenceParser::parseKeys($input));
    }

    /**
     * @return array<string, array{string, string}>
     */
    pub


class AnsiCodeTrackerTest extends TestCase
{
    private AnsiCodeTracker $tracker;

    protected function setUp(): void
    {
        $this->tracker = new AnsiCodeTracker();
    }

    // --------------------------------------------------
    // Basic SGR attributes on/off
    // --------------------------------------------------

    /**
     * @return iterable<string, array{int, int}>
     */
    public static function sgrAttributeOnOffProvider(): iterable
    {
        yield 'bold' => [1, 22];

gh' => [9, 29];
    }

    #[DataProvider('sgrAttributeOnOffProvider')]
    public function testSgrAttributeOnAndOff(int $onCode, int $offCode)
    {
        $this->tracker->process("\x1b[{$onCode}m");
        $this->assertTrue($this->tracker->hasActiveCod

 yield 'dim' => [2, 22];
        yield 'italic' => [3, 23];
        yield 'underline' => [4, 24];
        yield 'blink' => [5, 25];
        yield 'inverse' => [7, 27];
        yield 'hidden' => [8, 28];
        yield 'strikethrou


 Symfony\Component\Tui\Render\ChromeApplier;
use Symfony\Component\Tui\Render\RenderContext;
use Symfony\Component\Tui\Render\WidgetRendererInterface;
use Symfony\Component\Tui\Style\Border;
use Symfony\Component\Tui\Style\Padding;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Style\TextAlign;
use Symfony\Component\Tui\Widget\TextWidget;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class ChromeApplierTest extends TestCase
{
    // ---------------------------------------

    public function testComputeInnerDimensions(int $columns, int $rows, Style $style, array $expected)
    {
        $applier = $this->createApplier();

        $this->assertSame($expected, $applier->computeInnerDimensions($columns, $rows, $style));
    }


ge.
 *
 * (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\Tui\Tests\Widget\Figlet;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Exception\InvalidArgumentException;
use Symfony\Component\Tui\Widget\Figlet\FontRegistry;

class FontRegistryTest extends TestCase
{
    public function testGetBundledFont()
    {
        $registry = new FontRegis

R__, 3).'/Widget/Figlet/fonts';

        $registry->register('my-font', $fontsDir.'/small.flf');

        $this->assertTrue($registry->has('my-font'));
        $font = $registry->get('my-font');
        $this->assertSame(5, $font->getHeight()); // small fo

try();

        $font = $registry->get('big');

        $this->assertSame(8, $font->getHeight());
    }

    public function testRegisterCustomFont()
    {
        $registry = new FontRegistry();
        $fontsDir = \dirname(__DI


 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\Tui\Tests\Event;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Event\TickEvent;

class TickEventTest extends TestCase
{
    public function testDeltaTimeDefaultsToZero()
    {
        $event = new TickEvent();

        $this->assertSame(0.0, $ev

efault()
    {
        $event = new TickEvent();

        $this->assertFalse($event->hasBusyHint());
        $this->assertFalse($event->isBusy());
    }

    public function testSetBusyTrueMarksBusy()
    {
        $event = new TickEvent();
        $event-

ent->getDeltaTime());
    }

    public function testDeltaTimeCanBeProvided()
    {
        $event = new TickEvent(0.125);

        $this->assertSame(0.125, $event->getDeltaTime());
    }

    public function testHasNoBusyHintByD


 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\Tui\Tests\Terminal;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Terminal\Terminal;

class TerminalTest extends TestCase
{
    protected function setUp(): void
    {
        if ('\\' === \DIRECTORY_SEPARATOR) {
            $this->markTestSkipped('fireAndFor

dForget');

        $start = microtime(true);
        $method->invoke($terminal, ['sleep', '10']);
        $elapsed = microtime(true) - $start;

        // Should return nearly instantly, not wait 10 seconds
        $this->assertLessThan(1.0, $elapsed);

get uses Unix shell syntax and is only invoked on macOS.');
        }
    }

    public function testFireAndForgetDoesNotBlock()
    {
        $terminal = new Terminal();
        $method = new \ReflectionMethod($terminal, 'fireAn


buted with this source code.
 */

namespace Symfony\Component\Tui\Tests\Render;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Ansi\AnsiUtils;
use Symfony\Component\Tui\Render\Renderer;
use Symfony\Component\Tui\Style\TailwindStylesheet;
use Symfony\Component\Tui\Widget\ContainerWidget;
use Symfony\Component\Tui\Widget\TextWidget;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class TextAlignTest extends TestCase
{
    #[DataProvide

new TextWidget('Hi');
        $text->addStyleClass($styleClass);
        $root->add($text);

        $lines = $renderer->render($root, 10, 5);

        $this->assertCount(1, $lines);
        $this->assertSame($expectedVisible, AnsiUtils::stripAnsiCodes($li

r('textAlignProvider')]
    public function testTextAlignment(string $styleClass, string $expectedVisible)
    {
        $renderer = new Renderer(new TailwindStylesheet());

        $root = new ContainerWidget();
        $text =


ymfony\Component\Tui\Ansi\TextWrapper;

class TextWrapperTest extends TestCase
{
    /**
     * @param string[] $expected
     */
    #[DataProvider('wrapBasicProvider')]
    public function testWrapBasic(string $input, int $width, array $expected)
    {
        $this->assertSame($expected, TextWrapper::wrapTextWithAnsi($input, $width));
    }

    /**
     * @return iterable<string, array{string, int, string[]}>
     */
    public static function wrapBasicProvider(): iterable
    {
        yield 'short lin

", 20, ['Hello', 'World']];
        yield 'multiple spaces preserved' => ['Hello   World', 20, ['Hello   World']];
    }

    public function testWrapWithAnsiCodes()
    {
        $styled = "\x1b[31mHello World\x1b[0m";
        $lines = TextWrapper::wrapTe

e fits' => ['Hello', 20, ['Hello']];
        yield 'empty string' => ['', 20, ['']];
        yield 'long line wraps at word boundary' => ['Hello World', 5, ['Hello', 'World']];
        yield 'newlines preserved' => ["Hello\nWorld


buted with this source code.
 */

namespace Symfony\Component\Tui\Tests\Input;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Input\Key;
use Symfony\Component\Tui\Input\KeyParser;

class KeyParserTest extends TestCase
{
    private KeyParser $parser;

    protected function setUp(): void
    {
        $this->parser = new KeyParser();
    }

    #[DataProvider('parseKeyProvider')]
    public function testParseKey(string $input, string $expectedKey)

er(): iterable
    {
        yield 'enter (CR)' => ["\r", Key::ENTER];
        yield 'enter (LF)' => ["\n", Key::ENTER];
        yield 'escape' => ["\x1b", Key::ESCAPE];
        yield 'tab' => ["\t", Key::TAB];
        yield 'backspace' => ["\x7f", Key::BA

   {
        $result = $this->parser->parse($input);
        $this->assertSame($expectedKey, $result['key']);
    }

    /**
     * @return iterable<string, array{string, string}>
     */
    public static function parseKeyProvid


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests\Loop;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Loop\AdaptativeTicker;
use Symfony\Component\Tui\Loop\TickRuntimeInterface;

class AdaptativeTickerTest extends TestCase
{
    public function testRefreshSchedulesIdlePollingWhenTickStateIsUnknown()
    {
        $runtime = new TestTickRuntime();
        $ticker = new Adap

ticker));
        } finally {
            $ticker->stop();
        }
    }

    public function testRefreshDisablesPollingWhenExplicitlyIdleWithoutOtherWork()
    {
        $runtime = new TestTickRuntime();
        $ticker = new AdaptativeTicker($runtime);

tativeTicker($runtime);

        try {
            $ticker->refresh(true, false, null, true, null);

            $this->assertSame(0.25, $this->getCurrentInterval($ticker));
            $this->assertNotNull($this->getCallbackId($


umentException;
use Symfony\Component\Tui\Style\Color;

class ColorTest extends TestCase
{
    #[DataProvider('namedColorProvider')]
    public function testNamedColor(string $name, string $expectedFg, string $expectedBg)
    {
        $color = Color::named($name);
        $this->assertSame($expectedFg, $color->toForegroundCode());
        $this->assertSame($expectedBg, $color->toBackgroundCode());
    }

    /**
     * @return iterable<string, array{string, string, string}>
     */
    public static functi

on namedColorProvider(): iterable
    {
        yield 'black' => ['black', "\x1b[30m", "\x1b[40m"];
        yield 'red' => ['red', "\x1b[31m", "\x1b[41m"];
        yield 'green' => ['green', "\x1b[32m", "\x1b[42m"];
        yield


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget\Figlet;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Exception\InvalidArgumentException;
use Symfony\Component\Tui\Widget\Figlet\FigletFont;

class FigletFontTest extends TestCase
{
    private const FONTS_DIR = __D

=> ['small', 5];
        yield 'slant' => ['slant', 6];
        yield 'standard' => ['standard', 6];
        yield 'mini' => ['mini', 4];
    }

    #[DataProvider('bundledFontProvider')]
    public function testLoadBundledFont(string $name, int $expectedH

IR__.'/../../../Widget/Figlet/fonts';

    /**
     * @return iterable<string, array{string, int}>
     */
    public static function bundledFontProvider(): iterable
    {
        yield 'big' => ['big', 8];
        yield 'small'


<?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\Tui\Tests\Terminal;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Terminal\TeeTerminal;
use Symfony\Component\Tui\Terminal\VirtualTerminal;

class BellTest extends TestCase
{
    public function testBellWritesBelCharacter()
    {

irtualTerminal();
        $secondary = new VirtualTerminal();
        $tee = new TeeTerminal($primary, $secondary);

        $tee->bell();

        $this->assertSame("\x07", $primary->getOutput());
        $this->assertSame("\x07", $secondary->getOutput())

     $terminal = new VirtualTerminal();
        $terminal->bell();

        $this->assertSame("\x07", $terminal->getOutput());
    }

    public function testBellOnTeeTerminalWritesToBothTerminals()
    {
        $primary = new V


 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\RateLimiter;

use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
use Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException;

/**
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
interface LimiterInterface
{
    /**
     * Waits

    *
     * @param int        $tokens  the number of tokens required
     * @param float|null $maxTime maximum accepted waiting time in seconds
     *
     * @throws MaxWaitDurationExceededException if $maxTime is set and the process needs to wait longer

until the required number of tokens is available.
     *
     * The reserved tokens will be taken into account when calculating
     * future token consumptions. Do not use this method if you intend
     * to skip this process.


 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\RateLimiter;

use Symfony\Component\RateLimiter\Exception\RateLimitExceededException;

/**
 * @author Valentin Silvestre <vsilvestre.pro@gmail.com>
 */
class RateLimit
{
    public function __construct(
        private int $availableTokens,
        private \

  * @throws RateLimitExceededException if not accepted
     */
    public function ensureAccepted(): static
    {
        if (!$this->accepted) {
            throw new RateLimitExceededException($this);
        }

        return $this;
    }

    public fu

DateTimeImmutable $retryAfter,
        private bool $accepted,
        private int $limit,
    ) {
    }

    public function isAccepted(): bool
    {
        return $this->accepted;
    }

    /**
     * @return $this
     *


hp

/*
 * 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\RateLimiter\Exception;

use Symfony\Component\RateLimiter\RateLimit;

/**
 * @author Kevin Bond <kevinbond@gmail.com>
 */
class RateLimitExceededException extends \RuntimeException
{
    public function __construct(
        private RateLimit $rateLi

Limit;
    }

    public function getRetryAfter(): \DateTimeImmutable
    {
        return $this->rateLimit->getRetryAfter();
    }

    public function getRemainingTokens(): int
    {
        return $this->rateLimit->getRemainingTokens();
    }

    publi

mit,
        int $code = 0,
        ?\Throwable $previous = null,
    ) {
        parent::__construct('Rate Limit Exceeded', $code, $previous);
    }

    public function getRateLimit(): RateLimit
    {
        return $this->rate


ge.
 *
 * (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\RateLimiter\Policy;

use Symfony\Component\RateLimiter\LimiterStateInterface;

/**
 * State of a fixed window aligned to calendar boundaries.
 *
 * @internal
 */
final class CalendarAlignedWindow implements LimiterStateInterface
{
    private int $hitCount = 0;

    public function __construct(

s->id;
    }

    public function getPeriodStart(): \DateTimeImmutable
    {
        return $this->periodStart;
    }

    public function getPeriodEnd(): \DateTimeImmutable
    {
        return $this->periodEnd;
    }

    public function getExpirationTim

       private string $id,
        private int $maxSize,
        private \DateTimeImmutable $periodStart,
        private \DateTimeImmutable $periodEnd,
    ) {
    }

    public function getId(): string
    {
        return $thi


buted with this source code.
 */

namespace Symfony\Component\RateLimiter\Policy;

use Symfony\Component\Lock\LockInterface;
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
use Symfony\Component\RateLimiter\LimiterInterface;
use Symfony\Component\RateLimiter\RateLimit;
use Symfony\Component\RateLimiter\Reservation;
use Symfony\Component\RateLimiter\Storage\StorageInterface;
use Symfony\Component\RateLimiter\Util\TimeUtil;

/**
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
f

he window is aligned to a calendar starting at this datetime
     *                                          and resetting every $interval, instead of starting on the first hit.
     *                                          The anchor's timezone is prese

inal class FixedWindowLimiter implements LimiterInterface
{
    use ResetLimiterTrait;

    private int $intervalInSeconds;
    private \DateInterval $interval;

    /**
     * @param \DateTimeImmutable|null $anchorAt When set, t


hp

/*
 * 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\RateLimiter\Policy;

use Symfony\Component\RateLimiter\LimiterInterface;
use Symfony\Component\RateLimiter\RateLimit;
use Symfony\Component\RateLimiter\Reservation;

/**
 * Implements a non limiting limiter.
 *
 * This can be used in cases where an

oat $maxTime = null): Reservation
    {
        return new Reservation(microtime(true), new RateLimit(\PHP_INT_MAX, new \DateTimeImmutable(), true, \PHP_INT_MAX));
    }

    public function consume(int $tokens = 1): RateLimit
    {
        return new Rate

implementation requires a
 * limiter, but no rate limit should be enforced.
 *
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
final class NoLimiter implements LimiterInterface
{
    public function reserve(int $tokens = 1, ?fl


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\RateLimiter\Policy;

use Symfony\Component\RateLimiter\Util\TimeUtil;

/**
 * Data object representing the fill rate of a token bucket.
 *
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
final class Rate
{
    public function __construct(
        private \DateInterval $refillTime,
        private int $refillAmount = 1,
    ) {
    }

    public

('PT1M'), $rate);
    }

    public static function perHour(int $rate = 1): self
    {
        return new static(new \DateInterval('PT1H'), $rate);
    }

    public static function perDay(int $rate = 1): self
    {
        return new static(new \DateInter

 static function perSecond(int $rate = 1): self
    {
        return new static(new \DateInterval('PT1S'), $rate);
    }

    public static function perMinute(int $rate = 1): self
    {
        return new static(new \DateInterval


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\RateLimiter\Policy;

use Symfony\Component\RateLimiter\Exception\InvalidIntervalException;
use Symfony\Component\RateLimiter\LimiterStateInterface;

/**
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 *
 * @internal
 */
final class SlidingWindow implements LimiterStateInterface
{
    private int $hitCount = 0;
    private int $hitCountForLast

Exception(\sprintf('The interval must be positive integer, "%d" given.', $intervalInSeconds));
        }
        $this->windowEndAt = microtime(true) + $intervalInSeconds;
    }

    public static function createFromPreviousWindow(self $window, int $interv

Window = 0;
    private float $windowEndAt;

    public function __construct(
        private string $id,
        private int $intervalInSeconds,
    ) {
        if ($intervalInSeconds < 1) {
            throw new InvalidInterval


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\RateLimiter\Policy;

use Symfony\Component\Lock\LockInterface;
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
use Symfony\Component\RateLimiter\LimiterInterface;
use Symfony\Component\RateLimiter\RateLimit;
use Symfony\Component\RateLimiter\Reservation;
use Symfony\Component\RateLimiter\Storage\StorageInterface;
use

are currently 25% into
 * the current window. We have made 3 hits in the current window so far.
 * That means our sliding window hit count is (75% * 8) + 3 = 9.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
final class SlidingWindowLimiter impl

 Symfony\Component\RateLimiter\Util\TimeUtil;

/**
 * The sliding window algorithm will look at your last window and the current one.
 * It is good algorithm to reduce bursts.
 *
 * Example:
 * Last time window we did 8 hits. We


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\RateLimiter\Policy;

use Symfony\Component\RateLimiter\LimiterStateInterface;

/**
 * @author Wouter de Jong <wouter@wouterj.nl>
 *
 * @internal
 */
final class TokenBucket implements LimiterStateInterface
{
    private int $tokens;
    private int $burstSize;
    private float $timer;

    /**
     * @param string     $id            unique ident

null $timer         the current timer of the bucket, defaulting to microtime(true)
     */
    public function __construct(
        private string $id,
        int $initialTokens,
        private Rate $rate,
        ?float $timer = null,
    ) {
        if

ifier for this bucket
     * @param int        $initialTokens the initial number of tokens in the bucket (i.e. the max burst size)
     * @param Rate       $rate          the fill rate and time of this bucket
     * @param float|


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\RateLimiter\Policy;

use Symfony\Component\Lock\LockInterface;
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
use Symfony\Component\RateLimiter\LimiterInterface;
use Symfony\Component\RateLimiter\RateLimit;
use Symfony\Component\RateLimiter\Reservation;
use Symfony\Component\RateLimiter\Storage\StorageInterface;

/*

    private Rate $rate,
        StorageInterface $storage,
        ?LockInterface $lock = null,
    ) {
        $this->id = $id;
        $this->storage = $storage;
        $this->lock = $lock;
    }

    /**
     * Waits until the required number of tokens

*
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
final class TokenBucketLimiter implements LimiterInterface
{
    use ResetLimiterTrait;

    public function __construct(
        string $id,
        private int $maxBurst,


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\RateLimiter\Policy;

use Symfony\Component\RateLimiter\LimiterStateInterface;

/**
 * @author Wouter de Jong <wouter@wouterj.nl>
 *
 * @internal
 */
final class Window implements LimiterStateInterface
{
    private int $hitCount = 0;
    private int $maxSize;
    private float $timer;

    public function __construct(
        private string $id,

ring
    {
        return $this->id;
    }

    public function getExpirationTime(): ?int
    {
        // Keep the entry alive long enough for any reservation debt to be
        // carried forward, otherwise resets that span an idle interval would

        private int $intervalInSeconds,
        int $windowSize,
        ?float $timer = null,
    ) {
        $this->maxSize = $windowSize;
        $this->timer = $timer ?? microtime(true);
    }

    public function getId(): st


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\RateLimiter\Tests;

use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ClockMock;
use Symfony\Component\RateLimiter\CompoundLimiter;
use Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException;
use Symfony\Component\RateLimiter\Policy\FixedWindowLimiter;
use Symfony\Component\RateLimite

        ClockMock::register(InMemoryStorage::class);
    }

    public function testConsume()
    {
        $limiter1 = $this->createLimiter(4, new \DateInterval('PT1S'));
        $limiter2 = $this->createLimiter(8, new \DateInterval('PT10S'));
        $

r\Storage\InMemoryStorage;

#[Group('time-sensitive')]
class CompoundLimiterTest extends TestCase
{
    private InMemoryStorage $storage;

    protected function setUp(): void
    {
        $this->storage = new InMemoryStorage();


 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\RateLimiter\Tests;

use PHPUnit\Framework\TestCase;
use Symfony\Component\RateLimiter\CompoundLimiter;
use Symfony\Component\RateLimiter\CompoundRateLimiterFactory;
use Symfony\Component\RateLimiter\LimiterInterface;
use Symfony\Component\RateLimiter\RateLim

cts($this->once())
            ->method('create')
            ->with('foo')
            ->willReturn($this->createStub(LimiterInterface::class))
        ;
        $factory2 = $this->createMock(RateLimiterFactoryInterface::class);
        $factory2

iterFactoryInterface;

class CompoundRateLimiterFactoryTest extends TestCase
{
    public function testCreate()
    {
        $factory1 = $this->createMock(RateLimiterFactoryInterface::class);
        $factory1
            ->expe


 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\RateLimiter\Tests;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\RateLimiter\Policy\FixedWindowLimiter;
use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use

 => 'token_bucket',
            'limit' => 10,
            'rate' => ['interval' => '1 second'],
        ]);
        $limiter = $factory->create('127.0.0.1');

        $this->assertInstanceOf(TokenBucketLimiter::class, $limiter);
    }

    public function

 Symfony\Component\RateLimiter\Storage\StorageInterface;

class LimiterTest extends TestCase
{
    public function testTokenBucket()
    {
        $factory = $this->createFactory([
            'id' => 'test',
            'policy'


ge.
 *
 * (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\Tui\Input;

/**
 * Helper class for creating key identifiers.
 *
 * Provides constants and factory methods for keyboard input matching.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class Key
{
    // Special keys
    public const ESCAPE = 'escape';
    public

e';
    public const END = 'end';
    public const PAGE_UP = 'page_up';
    public const PAGE_DOWN = 'page_down';

    // Arrow keys
    public const UP = 'up';
    public const DOWN = 'down';
    public const LEFT = 'left';
    public const RIGHT = 'right

const ENTER = 'enter';
    public const TAB = 'tab';
    public const SPACE = 'space';
    public const BACKSPACE = 'backspace';
    public const DELETE = 'delete';
    public const INSERT = 'insert';
    public const HOME = 'hom


 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\RateLimiter\Tests;

use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ClockMock;
use Symfony\Component\RateLimiter\Exception\RateLimitExceededException;
use Symfony\Component\RateLimiter\RateLimit;

#[Group('time-sensitive

Limit, $rateLimit->ensureAccepted());
    }

    public function testEnsureAcceptedThrowsRateLimitExceptionIfNotAccepted()
    {
        $rateLimit = new RateLimit(10, $retryAfter = new \DateTimeImmutable(), false, 10);

        try {
            $rateLimi

')]
class RateLimitTest extends TestCase
{
    public function testEnsureAcceptedDoesNotThrowExceptionIfAccepted()
    {
        $rateLimit = new RateLimit(10, new \DateTimeImmutable(), true, 10);

        $this->assertSame($rate


buted with this source code.
 */

namespace Symfony\Component\RateLimiter\Tests;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ClockMock;
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
use Symfony\Component\RateLimiter\Policy\FixedWindowLimiter;
use Symfony\Component\RateLimiter\Policy\NoLimiter;
use Symfony\Component\RateLimiter\Policy\SlidingWindowLimiter;
use Symfony\Component\Ra

gProvider')]
    public function testValidConfig(string $expectedClass, array $config)
    {
        $factory = new RateLimiterFactory($config, new InMemoryStorage());
        $rateLimiter = $factory->create('key');
        $this->assertInstanceOf($expecte

teLimiter\Policy\TokenBucketLimiter;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;

class RateLimiterFactoryTest extends TestCase
{
    #[DataProvider('validConfi


miter\Util\TimeUtil;

#[Group('time-sensitive')]
class FixedWindowLimiterTest extends TestCase
{
    private InMemoryStorage $storage;

    protected function setUp(): void
    {
        $this->storage = new InMemoryStorage();

        ClockMock::register(InMemoryStorage::class);
        ClockMock::register(RateLimit::class);
        ClockMock::register(Window::class);
        ClockMock::register(FixedWindowLimiter::class);
    }

    public function testConsume()
    {
        $now = time();
        $limit

assertSame(10, $rateLimit->getLimit());
        $this->assertTrue($rateLimit->isAccepted());
        $rateLimit = $limiter->consume();
        $this->assertFalse($rateLimit->isAccepted());
        $this->assertSame(10, $rateLimit->getLimit());
        // W

er = $this->createLimiter();

        // fill 9 tokens in 45 seconds
        for ($i = 0; $i < 9; ++$i) {
            $limiter->consume();
            sleep(5);
        }

        $rateLimit = $limiter->consume();
        $this->


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\RateLimiter\Tests\Policy;

use PHPUnit\Framework\TestCase;
use Symfony\Component\RateLimiter\Policy\NoLimiter;
use Symfony\Component\RateLimiter\RateLimit;
use Symfony\Component\RateLimiter\Reservation;

class NoLimiterTest extends TestCase
{
    p

= new NoLimiter();
        $this->assertInstanceOf(RateLimit::class, $limiter->consume());
    }

    public function testReserve()
    {
        $limiter = new NoLimiter();
        $this->assertInstanceOf(Reservation::class, $limiter->reserve());
    }
}

ublic function testConsume()
    {
        $limiter


n 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\RateLimiter\Tests\Policy;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\RateLimiter\Policy\Rate;

class RateTest extends TestCase
{
    #[DataProvider('provideRate')]
    public function testFromString(Rate $rate)
    {
        $this->assertEquals($rate, Rate:

ic function provideRate(): iterable
    {
        yield [new Rate(new \DateInterval('PT15S'), 10)];
        yield [Rate::perSecond(10)];
        yield [Rate::perMinute(10)];
        yield [Rate::perHour(10)];
        yield [Rate::perDay(10)];
        yield

:fromString((string) $rate));
    }

    public stat


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\RateLimiter\Tests\Policy;

use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ClockMock;
use Symfony\Component\RateLimiter\Policy\SlidingWindowLimiter;
use Symfony\Component\RateLimiter\RateLimit;
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;

#[Group('time-sensitive')]
class SlidingWin

ckMock::register(RateLimit::class);
    }

    public function testConsume()
    {
        $limiter = $this->createLimiter();

        $limiter->consume(8);
        sleep(15);

        $rateLimit = $limiter->consume();
        $this->assertTrue($rateLimit-

dowLimiterTest extends TestCase
{
    private InMemoryStorage $storage;

    protected function setUp(): void
    {
        $this->storage = new InMemoryStorage();

        ClockMock::register(InMemoryStorage::class);
        Clo


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\RateLimiter\Tests\Policy;

use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ClockMock;
use Symfony\Component\RateLimiter\Exception\InvalidIntervalException;
use Symfony\Component\RateLimiter\Policy\SlidingWindow;

class SlidingWindowTestToStringGadget
{
    public static bool $fired = false;

$window = new SlidingWindow('foo', 10);
        $this->assertSame(2 * 10, $window->getExpirationTime());
        $this->assertSame(2 * 10, $window->getExpirationTime());

        $data = serialize($window);
        sleep(10);
        $cachedWindow = unseri

public function __toString(): string
    {
        self::$fired = true;

        return '';
    }
}

#[Group('time-sensitive')]
class SlidingWindowTest extends TestCase
{
    public function testGetExpirationTime()
    {


mfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
use Symfony\Component\RateLimiter\Policy\Rate;
use Symfony\Component\RateLimiter\Policy\TokenBucket;
use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter;
use Symfony\Component\RateLimiter\RateLimit;
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
use Symfony\Component\RateLimiter\Tests\Resources\DummyWindow;

#[Group('time-sensitive')]
class TokenBucketLimiterTest extends TestCase
{
    private InMemoryStorage $storag

ter(TokenBucket::class);
        ClockMock::register(RateLimit::class);
    }

    public function testReserve()
    {
        $limiter = $this->createLimiter();

        $this->assertEquals(0, $limiter->reserve(5)->getWaitDuration());
        $this->asser

e;

    protected function setUp(): void
    {
        $this->storage = new InMemoryStorage();

        ClockMock::register(TokenBucketLimiter::class);
        ClockMock::register(InMemoryStorage::class);
        ClockMock::regis


 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\RateLimiter\Tests\Policy;

use PHPUnit\Framework\TestCase;
use Symfony\Component\RateLimiter\Policy\TokenBucket;

class TokenBucketTestToStringGadget
{
    public static bool $fired = false;

    public function __toString(): string
    {
        self::$fire

            'tokens' => 0,
            'timer' => 0.0,
            'burstSize' => 10,
            'rate' => 'PT1M-10',
        ];
        $payload = \sprintf('O:%d:"%s":%d:{', \strlen(TokenBucket::class), TokenBucket::class, \count($data));
        foreach

d = true;

        return '';
    }
}

class TokenBucketTest extends TestCase
{
    public function testUnserializeRejectsObjectInTypedIdProperty()
    {
        $data = [
            'id' => new TokenBucketTestToStringGadget(),


 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\RateLimiter\Tests\Policy;

use PHPUnit\Framework\TestCase;
use Symfony\Component\RateLimiter\Policy\Window;

class WindowTestToStringGadget
{
    public static bool $fired = false;

    public function __toString(): string
    {
        self::$fired = true;

t' => 0,
            'intervalInSeconds' => 1,
            'maxSize' => 10,
            'timer' => 0.0,
        ];
        $payload = \sprintf('O:%d:"%s":%d:{', \strlen(Window::class), Window::class, \count($data));
        foreach ($data as $key => $value

        return '';
    }
}

class WindowTest extends TestCase
{
    public function testUnserializeRejectsObjectInTypedIdProperty()
    {
        $data = [
            'id' => new WindowTestToStringGadget(),
            'hitCoun


s 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\RateLimiter\Tests\Resources;

use Symfony\Component\RateLimiter\LimiterStateInterface;

class DummyWindow implements LimiterStateInterface
{
    private string $id;
    private ?int $expirationTime;

    public function __construct(string $id = 'test', ?int $expirationT

{
        $this->id = $id;
        $this->expirationTime = $expirationTime;
    }

    public function getId(): string
    {
        return $this->id;
    }

    public function getExpirationTime(): ?int
    {
        return $this->expirationTime;
    }
}


ge.
 *
 * (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\RateLimiter\Tests\Storage;

use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\RateLimiter\Policy\Window;
use Symfony\Component\RateLimiter\Storage\CacheStorage;

class CacheStorageTest extends TestCase
{
    public functi

is->exactly(2))->method('expiresAfter')->with(10);

        $pool->method('getItem')->willReturnMap([
            [sha1('test'), $cacheItem],
        ]);
        $pool->expects($this->exactly(2))->method('save')->with($cacheItem);

        $window = new Wi

on testSave()
    {
        $pool = $this->createMock(CacheItemPoolInterface::class);
        $storage = new CacheStorage($pool);

        $cacheItem = $this->createMock(CacheItemInterface::class);
        $cacheItem->expects($th


 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\RateLimiter\Storage;

use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\RateLimiter\LimiterStateInterface;

/**
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
class CacheStorage implements StorageInterface
{
    public function __construct(

limiterState);
        if (null !== ($expireAfter = $limiterState->getExpirationTime())) {
            $cacheItem->expiresAfter($expireAfter);
        }

        $this->pool->save($cacheItem);
    }

    public function fetch(string $limiterStateId): ?Limi

    private CacheItemPoolInterface $pool,
    ) {
    }

    public function save(LimiterStateInterface $limiterState): void
    {
        $cacheItem = $this->pool->getItem(sha1($limiterState->getId()));
        $cacheItem->set($


 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\RateLimiter\Storage;

use Symfony\Component\RateLimiter\LimiterStateInterface;

/**
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
class InMemoryStorage implements StorageInterface
{
    private array $buckets = [];

    public function save(LimiterStateInterface $lim

 if (!isset($this->buckets[$limiterStateId])) {
            return null;
        }

        [$expireAt, $limiterState] = $this->buckets[$limiterStateId];
        if (null !== $expireAt && $expireAt <= microtime(true)) {
            unset($this->buckets[$li

iterState): void
    {
        $this->buckets[$limiterState->getId()] = [$this->getExpireAt($limiterState), serialize($limiterState)];
    }

    public function fetch(string $limiterStateId): ?LimiterStateInterface
    {


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Mime;

use Egulias\EmailValidator\EmailValidator;
use Egulias\EmailValidator\Validation\MessageIDValidation;
use Egulias\EmailValidator\Validation\RFCValidation;
use Symfony\Component\Mime\Encoder\IdnAddressEncoder;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Exception\LogicException;
use Symfony\Comp

ing between the first < and last > as email address.
     * This allows to use a single string to construct an Address, which can be convenient to use in
     * config, and allows to have more readable config.
     * This does not try to cover all edge cas

onent\Mime\Exception\RfcComplianceException;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class Address
{
    /**
     * A regex that matches a structure like 'Name <email@address.com>'.
     * It matches anyth


/*
 * 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\Mime;

use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Part\AbstractPart;

/**
 * @author Kevin Bond <kevinbond@gmail.com>
 */
class DraftEmail extends Email
{
    public function __construct(?Headers $headers = null, ?AbstractPar

sage-ID headers.
     * These are added by the client that actually sends the email.
     */
    public function getPreparedHeaders(): Headers
    {
        $headers = clone $this->getHeaders();

        if (!$headers->has('MIME-Version')) {
            $h

t $body = null)
    {
        parent::__construct($headers, $body);

        $this->getHeaders()->addTextHeader('X-Unsent', '1');
    }

    /**
     * Override default behavior as draft emails do not require From/Sender/Date/Mes


art;
use Symfony\Component\Mime\Part\File;
use Symfony\Component\Mime\Part\Multipart\AlternativePart;
use Symfony\Component\Mime\Part\Multipart\MixedPart;
use Symfony\Component\Mime\Part\Multipart\RelatedPart;
use Symfony\Component\Mime\Part\TextPart;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Email extends Message
{
    public const PRIORITY_HIGHEST = 1;
    public const PRIORITY_HIGH = 2;
    public const PRIORITY_NORMAL = 3;
    public const PRIORITY_LOW = 4;
    public const PRIORIT

RIORITY_LOWEST => 'Lowest',
    ];

    /**
     * @var resource|string|null
     */
    private $text;

    private ?string $textCharset = null;

    /**
     * @var resource|string|null
     */
    private $html;

    private ?string $htmlCharset = null;

Y_LOWEST = 5;

    private const PRIORITY_MAP = [
        self::PRIORITY_HIGHEST => 'Highest',
        self::PRIORITY_HIGH => 'High',
        self::PRIORITY_NORMAL => 'Normal',
        self::PRIORITY_LOW => 'Low',
        self::P


n 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\RateLimiter;

/**
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
final class Reservation
{
    /**
     * @param float $timeToAct Unix timestamp in seconds when this reservation should act
     */
    public function __construct(
        private float $timeToAct,
        private RateLimit $rateLimit,
    ) {

        return $this->timeToAct;
    }

    public function getWaitDuration(): float
    {
        return max(0, (-microtime(true)) + $this->timeToAct);
    }

    public function getRateLimit(): RateLimit
    {
        return $this->rateLimit;
    }

 }

    public function getTimeToAct(): float
    {


 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\Tui\Tests\Loop;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Exception\InvalidArgumentException;
use Symfony\Component\Tui\Loop\PeriodicStepper;

class PeriodicStepperTest extends TestCase
{
    public function testConstructorRejectsInvalidInterval()
    {

s);
        PeriodicStepper::everyMs(0);
    }

    public function testAdvanceAccumulatesElapsedDelta()
    {
        $stepper = new PeriodicStepper(0.1, 5);

        $this->assertSame(0, $stepper->advance(0.05));
        $this->assertSame(1, $stepper->ad

        $this->expectException(InvalidArgumentException::class);
        new PeriodicStepper(0.0);
    }

    public function testEveryMsRejectsInvalidInterval()
    {
        $this->expectException(InvalidArgumentException::clas


n 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\Tui\Event;

use Symfony\Component\Tui\Widget\AbstractWidget;

/**
 * Event dispatched when a widget's value changes.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ChangeEvent extends AbstractEvent
{
    public function __construct(
        AbstractWidget $target,
        private

nstruct($target);
    }

    /**
     * Get the current value.
     */
    public function getValue(): string
    {
        return $this->value;
    }

    /**
     * Check if the current value is empty or contains only whitespace.
     */
    public funct

readonly string $value,
    ) {
        parent::__co


he 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\Tui\Event;

use Symfony\Contracts\EventDispatcher\Event;

/**
 * Event dispatched when raw terminal input is received.
 *
 * Dispatched before focus navigation and before the focused widget
 * receives input. Call {@see stopPropagation()} to consume the input
 * and prevent furthe

en Potencier <fabien@symfony.com>
 */
class InputEvent extends Event
{
    public function __construct(
        private readonly string $data,
    ) {
    }

    /**
     * The raw input data from the terminal.
     */
    public function getData(): string

r processing.
 *
 * @experimental
 *
 * @author Fabi


 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\Tui\Event;

use Symfony\Component\Tui\Widget\SelectListWidget;

/**
 * Event dispatched when the highlighted item changes in a SelectList.
 *
 * This fires when the user moves the cursor (arrow keys, scroll), not
 * when they confirm a selection (that's {@see SelectEvent

public function __construct(
        SelectListWidget $target,
        private readonly array $item,
    ) {
        parent::__construct($target);
    }

    /**
     * Get the full highlighted item array.
     *
     * @return array{value: string, label:

}).
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class SelectionChangeEvent extends AbstractEvent
{
    /**
     * @param array{value: string, label: string, description?: string} $item
     */


<?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\Tui\Event;

use Symfony\Component\Tui\Widget\AbstractWidget;

/**
 * Event dispatched when content is submitted (e.g., Enter pressed in Input/Editor).
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class SubmitEvent e

    public function getValue(): string
    {
        return $this->value;
    }

    /**
     * Check if the submitted value is empty or contains only whitespace.
     */
    public function isBlank(): bool
    {
        return '' === trim($this->value);

xtends AbstractEvent
{
    public function __construct(
        AbstractWidget $target,
        private readonly string $value,
    ) {
        parent::__construct($target);
    }

    /**
     * Get the submitted value.
     */


 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\Tui\Input;

/**
 * Configurable keybindings manager.
 *
 * Maps action names to key identifiers, allowing customizable keybindings.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class Keybindings
{
    /** @var array<string, string[]>

->bindings = $bindings;
        $this->parser = $parser ?? new KeyParser();
    }

    public function matches(string $data, string $action): bool
    {
        if (!isset($this->bindings[$action])) {
            return false;
        }

        foreach ($

*/
    private array $bindings;

    private KeyParser $parser;

    /**
     * @param array<string, string[]> $bindings
     */
    public function __construct(array $bindings = [], ?KeyParser $parser = null)
    {
        $this


rray<int, array<int, array{char: string, style: string}>> */
    private array $cells = [];
    private int $cursorRow = 0;
    private int $cursorCol = 0;
    private int $width;
    private int $height;
    private AnsiCodeTracker $styleTracker;

    public function __construct(int $width = 80, int $height = 24)
    {
        $this->width = $width;
        $this->height = $height;
        $this->styleTracker = new AnsiCodeTracker();
        $this->clear();
    }

    /**
     * Clear the screen buffer.

 = 0;
        $this->styleTracker->reset();
    }

    /**
     * Process terminal output and update the screen buffer.
     */
    public function write(string $data): void
    {
        $i = 0;
        $len = \strlen($data);

        while ($i < $len) {

   */
    public function clear(): void
    {
        $this->cells = [];
        for ($row = 0; $row < $this->height; ++$row) {
            $this->cells[$row] = [];
        }
        $this->cursorRow = 0;
        $this->cursorCol


* @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class Terminal implements TerminalInterface
{
    private ?StdinBuffer $stdinBuffer = null;

    private string $initialSttyState = '';
    private bool $kittyProtocolActive = false;
    private bool $started = false;
    private ?string $stdinCallbackId = null;
    private ?string $signalCallbackId = null;

    /** @var (\Closure(string): void)|null */
    private ?\Closure $onInput = null;

    /** @var (\Closure(): void)|null *

 = null;
    private ?int $cachedRows = null;

    public function start(callable $onInput, callable $onResize, callable $onKittyProtocolActivated): void
    {
        if ($this->started) {
            return;
        }

        $this->onInput = $onInput(.

/
    private ?\Closure $onResize = null;

    /** @var (\Closure(): void)|null */
    private ?\Closure $onKittyProtocolActivated = null;

    // Cached terminal dimensions (refreshed on SIGWINCH)
    private ?int $cachedColumns


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Terminal;

use Symfony\Component\Tui\Input\StdinBuffer;

/**
 * Virtual terminal for testing.
 *
 * Captures output and allows simulating input for unit tests.
 * Uses StdinBuffer for input parsing to match real Terminal behavior.
 *
 * Virtual terminals don't have a physical screen: they don't scroll
 * content out of the addressable area an

lTerminal implements TerminalInterface
{
    private string $output = '';
    private ?StdinBuffer $stdinBuffer = null;
    private ?\Closure $onResize = null;

    public function __construct(
        private int $columns = 80,
        private int $rows =

d don't respond to terminal
 * queries (e.g. cell size). This is used by the rendering pipeline
 * to adjust its behavior accordingly.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class Virtua


 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\Tui\Loop;

use Symfony\Component\Tui\Exception\InvalidArgumentException;

/**
 * Converts elapsed time into bounded fixed-step counts.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class FixedStepAccumulator
{
    priva

Exception(\sprintf('Steps per second must be greater than 0, got %d.', $stepsPerSecond));
        }

        if ($maxStepsPerUpdate < 1) {
            throw new InvalidArgumentException(\sprintf('Max steps per update must be greater than 0, got %d.', $maxS

te float $accumulator = 0.0;

    public function __construct(
        private float $stepsPerSecond,
        private int $maxStepsPerUpdate = 5,
    ) {
        if ($stepsPerSecond <= 0.0) {
            throw new InvalidArgument


 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\Tui\Loop;

/**
 * Small monotonic-ish clock abstraction for game and animation loops.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class LoopClock
{
    private float $time;

    public function __construc

eltaTime = null): float
    {
        if (null === $deltaTime) {
            $now = hrtime(true) / 1e9;
            $elapsed = max(0.0, $now - $this->time);
            $this->time = $now;

            return $elapsed;
        }

        $elapsed = max(0.0

t(
        ?float $time = null,
    ) {
        $this->time = $time ?? hrtime(true) / 1e9;
    }

    /**
     * Advance clock state and return elapsed seconds since previous advance.
     */
    public function advance(?float $d


 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\RateLimiter;

use Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException;

/**
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
final class CompoundLimiter implements LimiterInterface
{
    /**
     * @param LimiterInterface[] $limiters
     */
    public

ublic function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
    {
        throw new ReserveNotSupportedException(__CLASS__);
    }

    public function consume(int $tokens = 1): RateLimit
    {
        $minimalRateLimit = null;
        for

function __construct(
        private array $limiters,
    ) {
        if (!$limiters) {
            throw new \LogicException(\sprintf('"%s::%s()" require at least one limiter.', self::class, __METHOD__));
        }
    }

    p


buted with this source code.
 */

namespace Symfony\Component\RateLimiter;

use Symfony\Component\Lock\LockFactory;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\RateLimiter\Policy\FixedWindowLimiter;
use Symfony\Component\RateLimiter\Policy\NoLimiter;
use Symfony\Component\RateLimiter\Policy\Rate;
use Symfony\Component\RateLimiter\Policy\SlidingWindowLimiter;
use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter;
use Symfon

nstruct(
        array $config,
        private StorageInterface $storage,
        private ?LockFactory $lockFactory = null,
    ) {
        $options = new OptionsResolver();
        self::configureOptions($options);

        $this->config = $options->reso

y\Component\RateLimiter\Storage\StorageInterface;

/**
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
final class RateLimiterFactory implements RateLimiterFactoryInterface
{
    private array $config;

    public function __co


hp

/*
 * 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\Tui\Tests\Loop;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Loop\LoopClock;

class LoopClockTest extends TestCase
{
    public function testAdvanceWithExplicitDeltaAdvancesInternalTime()
    {
        $clock = new LoopClock(100.0);

ock = new LoopClock();
        usleep(1000);

        $elapsed = $clock->advance();

        $this->assertGreaterThan(0.0, $elapsed);
    }

    public function testResetSetsCurrentTime()
    {
        $clock = new LoopClock(100.0);
        $clock->reset(4

       $elapsed = $clock->advance(0.25);

        $this->assertSame(0.25, $elapsed);
        $this->assertSame(100.25, $clock->now());
    }

    public function testAdvanceWithoutDeltaUsesWallClockElapsedTime()
    {
        $cl


ge.
 *
 * (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\Tui\Tests\Loop;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Exception\InvalidArgumentException;
use Symfony\Component\Tui\Loop\TickScheduler;

class TickSchedulerTest extends TestCase
{
    public function testScheduleRejectsNonPositiveIntervals()
    {
        $scheduler = new Tic

$scheduler = new TickScheduler();
        $calls = 0;
        $start = microtime(true);

        $scheduler->schedule(static function () use (&$calls): void {
            ++$calls;
        }, 0.5);

        $scheduler->runDue($start + 0.10);
        $this-

kScheduler();

        $this->expectException(InvalidArgumentException::class);
        $scheduler->schedule(static function (): void {}, 0.0);
    }

    public function testRunDueExecutesAndReschedulesCallbacks()
    {


/*
 * 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\Tui\Event;

use Symfony\Component\Tui\Widget\AbstractWidget;
use Symfony\Component\Tui\Widget\FocusableInterface;

/**
 * Event dispatched when focus changes to a new widget.
 *
 * Not dispatched when focus is cleared to null. Listeners that need
 * to

/
class FocusEvent extends AbstractEvent
{
    public function __construct(
        AbstractWidget&FocusableInterface $target,
        private readonly ?FocusableInterface $previous,
    ) {
        parent::__construct($target);
    }

    /**
     * Get t

 know when focus is lost can observe widget removal directly or
 * track the previous focus via {@see getPrevious()} on subsequent
 * focus-change events.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *


/*
 * 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\Tui\Event;

/**
 * Event dispatched on each tick of the main loop.
 *
 * Unlike widget events, tick is a global application event
 * with no associated widget target.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Tic

previous tick callback.
     */
    public function getDeltaTime(): float
    {
        return $this->deltaTime;
    }

    public function setBusy(bool $busy = true): void
    {
        $this->hasBusyHint = true;
        $this->busy = $busy;
    }

    pu

kEvent
{
    private bool $hasBusyHint = false;
    private bool $busy = false;

    public function __construct(
        private readonly float $deltaTime = 0.0,
    ) {
    }

    /**
     * Time elapsed (in seconds) since the


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Terminal;

/**
 * Terminal that delegates to multiple terminals simultaneously.
 *
 * This is useful for:
 * - Running examples with both a real terminal and a VirtualTerminal for testing
 * - Recording terminal output while displaying it
 * - Debugging terminal output
 *
 * The primary terminal is used for input handling and dimension querie

he primary terminal (used for input and dimensions)
     * @param TerminalInterface $secondary Additional terminal that receives writes
     */
    public function __construct(
        private TerminalInterface $primary,
        private TerminalInterface $

s.
 * All terminals receive write operations.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class TeeTerminal implements TerminalInterface
{
    /**
     * @param TerminalInterface $primary   T


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Loop;

use Revolt\EventLoop;

/**
 * Drives the main TUI tick interval using adaptive scheduling.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class AdaptativeTicker
{
    private const float MIN_INTERVAL = 0.001;

    private ?string $callbackId = null;
    private ?float $interval = null;

lic function refresh(bool $running, bool $renderRequested, ?float $nextScheduledDelay, bool $hasTickCallback, ?bool $lastTickBusyHint): void
    {
        $this->setInterval($this->computeDesiredInterval($running, $renderRequested, $nextScheduledDelay, $ha

    public function __construct(
        private readonly TickRuntimeInterface $runtime,
        private readonly float $activeTickInterval = 0.01,
        private readonly float $idleTickInterval = 0.25,
    ) {
    }

    pub


ge.
 *
 * (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\Tui\Loop;

use Symfony\Component\Tui\Exception\InvalidArgumentException;

/**
 * Internal scheduler for repeat callbacks executed from the TUI tick.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class TickScheduler
{
    private int $counter = 0

void $callback
     */
    public function schedule(callable $callback, float $intervalSeconds): string
    {
        if ($intervalSeconds <= 0) {
            throw new InvalidArgumentException(\sprintf('Interval must be greater than 0, got %f.', $interval

;

    /**
     * @var array<string, array{
     *     callback: callable(): void,
     *     interval: float,
     *     next_run_at: float
     * }>
     */
    private array $intervals = [];

    /**
     * @param callable():


 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\Tui\Event;

use Symfony\Component\Tui\Widget\SelectListWidget;

/**
 * Event dispatched when an item is selected in a SelectList.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class SelectEvent extends AbstractEvent
{
    /**

get);
    }

    /**
     * Get the full selected item array.
     *
     * @return array{value: string, label: string, description?: string}
     */
    public function getItem(): array
    {
        return $this->item;
    }

    /**
     * Get the selec

   * @param array{value: string, label: string, description?: string} $item
     */
    public function __construct(
        SelectListWidget $target,
        private readonly array $item,
    ) {
        parent::__construct($tar


he 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\Tui\Event;

use Symfony\Component\Tui\Widget\AbstractWidget;
use Symfony\Contracts\EventDispatcher\Event as BaseEvent;

/**
 * Base class for all TUI widget events.
 *
 * Extends Symfony's Event so it can be dispatched through
 * Symfony's EventDispatcher. Carries the target widge

 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
abstract class AbstractEvent extends BaseEvent
{
    public function __construct(
        private readonly AbstractWidget $target,
    ) {
    }

    public function getTarget(): AbstractWidget
    {

t that
 * originated the event.
 *
 * @experimental


 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\Tui\Event;

use Symfony\Component\Tui\Widget\SettingsListWidget;

/**
 * Event dispatched when a setting value changes in SettingsList.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class SettingChangeEvent extends AbstractEvent

ing identifier.
     */
    public function getId(): string
    {
        return $this->id;
    }

    /**
     * Get the new setting value.
     */
    public function getValue(): string
    {
        return $this->value;
    }

    /**
     * Check if th

{
    public function __construct(
        SettingsListWidget $target,
        private readonly string $id,
        private readonly string $value,
    ) {
        parent::__construct($target);
    }

    /**
     * Get the sett


ge.
 *
 * (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\Tui\Terminal;

/**
 * Interface for terminal implementations.
 *
 * Provides abstraction over terminal I/O for the TUI framework.
 * Implementations handle raw mode, input reading, and output writing.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface TerminalIn

(string): void $onInput                  Called when input is received
     * @param callable(): void       $onResize                 Called when terminal is resized
     * @param callable(): void       $onKittyProtocolActivated Called when Kitty keyboard

terface
{
    /**
     * Start the terminal with input and resize handlers.
     *
     * This typically enables raw mode, sets up signal handlers,
     * and prepares the terminal for TUI operation.
     *
     * @param callable


he 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\RateLimiter;

/**
 * @author Kevin Bond <kevinbond@gmail.com>
 */
final class CompoundRateLimiterFactory implements RateLimiterFactoryInterface
{
    /**
     * @param iterable<RateLimiterFactoryInterface> $rateLimiterFactories
     */
    public function __construct(private itera

unction create(?string $key = null): LimiterInterface
    {
        $rateLimiters = [];

        foreach ($this->rateLimiterFactories as $rateLimiterFactory) {
            $rateLimiters[] = $rateLimiterFactory->create($key);
        }

        return new C

ble $rateLimiterFactories)
    {
    }

    public f


aking key parsing work correctly.
 * Also handles bracketed paste mode.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class StdinBuffer
{
    private const int MAX_PASTE_BYTES = 16 * 1024 * 1024;
    private const int MAX_PENDING_BYTES = 1024 * 1024;
    private const string PASTE_OVERFLOW_MESSAGE = '[paste exceeded 16 MiB limit]';

    private string $buffer = '';

    /** @var (\Closure(string): void)|null */
    private ?\Closure $onData = null;

    /*

ram callable(string): void $callback
     */
    public function onData(callable $callback): void
    {
        $this->onData = $callback(...);
    }

    /**
     * Set callback for paste content.
     *
     * @param callable(string): void $callback

* @var (\Closure(string): void)|null */
    private ?\Closure $onPaste = null;

    private bool $inPaste = false;
    private string $pasteBuffer = '';

    /**
     * Set callback for individual key sequences.
     *
     * @pa


ge.
 *
 * (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\Tui\Loop;

use Symfony\Component\Tui\Exception\InvalidArgumentException;

/**
 * Converts elapsed time into periodic fixed-step counts.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class PeriodicStepper
{
    private FixedStepAccumulator $accum

entException(\sprintf('Interval must be greater than 0, got %d.', $intervalSeconds));
        }

        $this->accumulator = new FixedStepAccumulator(1.0 / $intervalSeconds, $maxStepsPerUpdate);
        $this->clock = new LoopClock();
    }

    public st

ulator;
    private LoopClock $clock;

    public function __construct(
        private float $intervalSeconds,
        int $maxStepsPerUpdate = 8,
    ) {
        if ($intervalSeconds <= 0.0) {
            throw new InvalidArgum


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Mime\Part\Multipart;

use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Part\AbstractMultipartPart;
use Symfony\Component\Mime\Part\TextPart;

/**
 * Implements RFC 7578.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class FormDataPart extends AbstractMultipartPart
{
    /**
     * @param ar

ers()->setMaxLineLength(\PHP_INT_MAX);
    }

    public function getMediaSubtype(): string
    {
        return 'form-data';
    }

    public function getParts(): array
    {
        return $this->prepareFields($this->fields);
    }

    private function

ray<string|array|TextPart> $fields
     */
    public function __construct(
        private array $fields = [],
    ) {
        parent::__construct();

        // HTTP does not support \r\n in header values
        $this->getHead


*
 * 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\Mime\Tests\Part\Multipart;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\Part\MessagePart;
use Symfony\Component\Mime\Part\Multipart\DigestPart;

class DigestPartTest extends TestCase
{
    public functio

t($a = new MessagePart(new Message()), $b = new MessagePart(new Message()));
        $this->assertEquals('multipart', $r->getMediaType());
        $this->assertEquals('digest', $r->getMediaSubtype());
        $this->assertEquals([$a, $b], $r->getParts());

n testConstructor()
    {
        $r = new DigestPar


buted with this source code.
 */

namespace Symfony\Component\Mime\Tests\Part\Multipart;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
use Symfony\Component\Mime\Part\TextPart;

class FormDataPartTest extends TestCase
{
    public function testConstructor()
    {
        $r = new \ReflectionProperty(TextPart::class, 'encoding');

        $b = new TextPart('conte

ore than 76 characters, ok?',
            'bar' => clone $b,
            'baz' => clone $c,
        ]);
        $this->assertEquals('multipart', $f->getMediaType());
        $this->assertEquals('form-data', $f->getMediaSubtype());
        $t = new TextPart

nt');
        $c = DataPart::fromPath($file = __DIR__.'/../../Fixtures/mimetypes/test.gif');
        $f = new FormDataPart([
            'foo' => $content = 'very very long content that will not be cut even if the length is way m


hp

/*
 * 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\Mime\Tests\Part\Multipart;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Part\Multipart\RelatedPart;
use Symfony\Component\Mime\Part\TextPart;

class RelatedPartTest extends TestCase
{
    public function testConstructor()
    {

     $this->assertEquals('related', $r->getMediaSubtype());
        $this->assertEquals([$a, $b, $c], $r->getParts());
        $this->assertFalse($a->getHeaders()->has('Content-ID'));
        $this->assertTrue($b->getHeaders()->has('Content-ID'));

  $r = new RelatedPart($a = new TextPart('content'), $b = new TextPart('HTML content', 'utf-8', 'html'), $c = new TextPart('HTML content again', 'utf-8', 'html'));
        $this->assertEquals('multipart', $r->getMediaType());


 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\Mime\Tests\Test\Constraint;

use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Test\Constraint\EmailAddressContains;

clas

value "expectedValue"', $constraint->toString());
    }

    public function testFailureDescription()
    {
        $mailboxHeader = 'text@example.com';
        $headers = new Headers();
        $headers->addMailboxHeader($mailboxHeader, 'actualValue@examp

s EmailAddressContainsTest extends TestCase
{
    public function testToString()
    {
        $constraint = new EmailAddressContains('headerName', 'expectedValue');

        $this->assertSame('contains address "headerName" with


hp

/*
 * 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\Mime\Tests\Test\Constraint;

use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Test\Constraint\EmailAttachmentCount;

class EmailAttachmentCountTest extends

ription()
    {
        $email = new Email();
        $email->attach('attachment content', 'attachment.txt');

        $this->expectException(ExpectationFailedException::class);
        $this->expectExceptionMessage('Failed asserting that the Email has sen

 TestCase
{
    public function testToString()
    {
        $constraint = new EmailAttachmentCount(1);

        $this->assertSame('has sent "1" attachment(s)', $constraint->toString());
    }

    public function testFailureDesc