From f0c075cc71995a10dd9c04db279e32b491ec1e4c Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Mon, 16 Apr 2018 21:21:40 +0200 Subject: [PATCH 01/16] Refactor IO --- src/CliMenu.php | 2 +- src/Dialogue/Dialogue.php | 2 +- src/IO/BufferedOutput.php | 32 +++++++ src/IO/InputStream.php | 15 ++++ src/IO/OutputStream.php | 11 +++ src/IO/ResourceInputStream.php | 43 ++++++++++ src/IO/ResourceOutputStream.php | 42 +++++++++ src/Terminal/TerminalFactory.php | 5 +- src/Terminal/TerminalInterface.php | 8 +- src/Terminal/UnixTerminal.php | 43 +++++++--- test/CliMenuTest.php | 68 ++++++++------- test/Dialogue/ConfirmTest.php | 122 +++++++++++---------------- test/Dialogue/FlashTest.php | 82 +++++++++--------- test/IO/BufferedOutputTest.php | 50 +++++++++++ test/IO/ResourceInputStreamTest.php | 44 ++++++++++ test/IO/ResourceOutputStreamTest.php | 36 ++++++++ 16 files changed, 447 insertions(+), 158 deletions(-) create mode 100644 src/IO/BufferedOutput.php create mode 100644 src/IO/InputStream.php create mode 100644 src/IO/OutputStream.php create mode 100644 src/IO/ResourceInputStream.php create mode 100644 src/IO/ResourceOutputStream.php create mode 100644 test/IO/BufferedOutputTest.php create mode 100644 test/IO/ResourceInputStreamTest.php create mode 100644 test/IO/ResourceOutputStreamTest.php diff --git a/src/CliMenu.php b/src/CliMenu.php index 16c9bb5f..e3dad919 100644 --- a/src/CliMenu.php +++ b/src/CliMenu.php @@ -254,7 +254,7 @@ protected function draw() : void $frame->newLine(2); foreach ($frame->getRows() as $row) { - echo $row; + $this->terminal->getOutput()->write($row); } $this->currentFrame = $frame; diff --git a/src/Dialogue/Dialogue.php b/src/Dialogue/Dialogue.php index 34f3c113..3215cb0c 100644 --- a/src/Dialogue/Dialogue.php +++ b/src/Dialogue/Dialogue.php @@ -101,7 +101,7 @@ protected function emptyRow() : void protected function write(string $text, int $column = null) : void { $this->terminal->moveCursorToColumn($column ?: $this->x); - echo $text; + $this->terminal->getOutput()->write($text); } public function getStyle() : MenuStyle diff --git a/src/IO/BufferedOutput.php b/src/IO/BufferedOutput.php new file mode 100644 index 00000000..1f07cb2a --- /dev/null +++ b/src/IO/BufferedOutput.php @@ -0,0 +1,32 @@ + + */ +class BufferedOutput implements OutputStream +{ + private $buffer = ''; + + public function write(string $buffer): void + { + $this->buffer .= $buffer; + } + + public function fetch(bool $clean = true) : string + { + $buffer = $this->buffer; + + if ($clean) { + $this->buffer = ''; + } + + return $buffer; + } + + public function __toString() : string + { + return $this->fetch(); + } +} diff --git a/src/IO/InputStream.php b/src/IO/InputStream.php new file mode 100644 index 00000000..009f1a09 --- /dev/null +++ b/src/IO/InputStream.php @@ -0,0 +1,15 @@ + + */ +interface InputStream +{ + /** + * Callback should be called with the number of bytes requested + * when ready. + */ + public function read(int $numBytes, callable $callback) : void; +} diff --git a/src/IO/OutputStream.php b/src/IO/OutputStream.php new file mode 100644 index 00000000..af43e124 --- /dev/null +++ b/src/IO/OutputStream.php @@ -0,0 +1,11 @@ + + */ +interface OutputStream +{ + public function write(string $buffer) : void; +} diff --git a/src/IO/ResourceInputStream.php b/src/IO/ResourceInputStream.php new file mode 100644 index 00000000..430da936 --- /dev/null +++ b/src/IO/ResourceInputStream.php @@ -0,0 +1,43 @@ + + */ +class ResourceInputStream implements InputStream +{ + /** + * @var resource + */ + private $stream; + + public function __construct($stream = null) + { + if ($stream === null) { + $stream = STDIN; + } + + if (!is_resource($stream) || get_resource_type($stream) !== 'stream') { + throw new \InvalidArgumentException('Expected a valid stream'); + } + + $meta = stream_get_meta_data($stream); + if (strpos($meta['mode'], 'r') === false && strpos($meta['mode'], '+') === false) { + throw new \InvalidArgumentException('Expected a readable stream'); + } + + $this->stream = $stream; + } + + public function read(int $numBytes, callable $callback) : void + { + $buffer = fread($this->stream, $numBytes); + $callback($buffer); + } +} diff --git a/src/IO/ResourceOutputStream.php b/src/IO/ResourceOutputStream.php new file mode 100644 index 00000000..3bef8176 --- /dev/null +++ b/src/IO/ResourceOutputStream.php @@ -0,0 +1,42 @@ + + */ +class ResourceOutputStream implements OutputStream +{ + /** + * @var resource + */ + private $stream; + + public function __construct($stream = null) + { + if ($stream === null) { + $stream = STDOUT; + } + + if (!is_resource($stream) || get_resource_type($stream) !== 'stream') { + throw new \InvalidArgumentException('Expected a valid stream'); + } + + $meta = stream_get_meta_data($stream); + if (strpos($meta['mode'], 'r') !== false && strpos($meta['mode'], '+') === false) { + throw new \InvalidArgumentException('Expected a writable stream'); + } + + $this->stream = $stream; + } + + public function write(string $buffer): void + { + fwrite($this->stream, $buffer); + } +} diff --git a/src/Terminal/TerminalFactory.php b/src/Terminal/TerminalFactory.php index 250d4a7f..779a1c14 100644 --- a/src/Terminal/TerminalFactory.php +++ b/src/Terminal/TerminalFactory.php @@ -2,6 +2,9 @@ namespace PhpSchool\CliMenu\Terminal; +use PhpSchool\CliMenu\IO\ResourceInputStream; +use PhpSchool\CliMenu\IO\ResourceOutputStream; + /** * @author Michael Woodward */ @@ -9,6 +12,6 @@ class TerminalFactory { public static function fromSystem() : TerminalInterface { - return new UnixTerminal(); + return new UnixTerminal(new ResourceInputStream, new ResourceOutputStream); } } diff --git a/src/Terminal/TerminalInterface.php b/src/Terminal/TerminalInterface.php index 85adda05..1ad18ad4 100644 --- a/src/Terminal/TerminalInterface.php +++ b/src/Terminal/TerminalInterface.php @@ -1,6 +1,7 @@ @@ -85,5 +86,10 @@ public function disableCursor() : void; /** * @return string */ - public function getKeyedInput() : string; + public function getKeyedInput(array $map = []) : ?string; + + /** + * Get the output stream + */ + public function getOutput() : OutputStream; } diff --git a/src/Terminal/UnixTerminal.php b/src/Terminal/UnixTerminal.php index 0e701ed3..0a96e584 100644 --- a/src/Terminal/UnixTerminal.php +++ b/src/Terminal/UnixTerminal.php @@ -1,6 +1,8 @@ @@ -37,13 +39,25 @@ class UnixTerminal implements TerminalInterface */ private $originalConfiguration; + /** + * @var InputStream + */ + private $input; + + /** + * @var OutputStream + */ + private $output; + /** * Initialise the terminal from resource * */ - public function __construct() + public function __construct(InputStream $input, OutputStream $output) { $this->getOriginalConfiguration(); + $this->input = $input; + $this->output = $output; } /** @@ -129,7 +143,7 @@ public function supportsColour() : bool return $this->isTTY(); } - public function getKeyedInput() : string + public function getKeyedInput(array $map = []) : ?string { // TODO: Move to class var? // TODO: up, down, enter etc in Abstract CONSTs @@ -145,7 +159,11 @@ public function getKeyedInput() : string " " => 'enter', ]; - $input = fread(STDIN, 4); + $input = ''; + $this->input->read(4, function ($buffer) use (&$input) { + $input .= $buffer; + }); + $this->clearLine(); return array_key_exists($input, $map) @@ -158,7 +176,7 @@ public function getKeyedInput() : string */ public function clear() : void { - echo "\033[2J"; + $this->output->write("\033[2J"); } /** @@ -166,7 +184,7 @@ public function clear() : void */ public function enableCursor() : void { - echo "\033[?25h"; + $this->output->write("\033[?25h"); } /** @@ -174,7 +192,7 @@ public function enableCursor() : void */ public function disableCursor() : void { - echo "\033[?25l"; + $this->output->write("\033[?25l"); } /** @@ -184,7 +202,7 @@ public function disableCursor() : void */ public function moveCursorToTop() : void { - echo "\033[H"; + $this->output->write("\033[H"); } /** @@ -192,7 +210,7 @@ public function moveCursorToTop() : void */ public function moveCursorToRow(int $rowNumber) : void { - echo sprintf("\033[%d;0H", $rowNumber); + $this->output->write(sprintf("\033[%d;0H", $rowNumber)); } /** @@ -200,7 +218,7 @@ public function moveCursorToRow(int $rowNumber) : void */ public function moveCursorToColumn(int $column) : void { - echo sprintf("\033[%dC", $column); + $this->output->write(sprintf("\033[%dC", $column)); } /** @@ -208,7 +226,7 @@ public function moveCursorToColumn(int $column) : void */ public function clearLine() : void { - echo sprintf("\033[%dD\033[K", $this->getWidth()); + $this->output->write(sprintf("\033[%dD\033[K", $this->getWidth())); } /** @@ -221,4 +239,9 @@ public function clean() : void $this->clearLine(); } } + + public function getOutput() : OutputStream + { + return $this->output; + } } diff --git a/test/CliMenuTest.php b/test/CliMenuTest.php index dd655ef6..abc4bbb0 100644 --- a/test/CliMenuTest.php +++ b/test/CliMenuTest.php @@ -4,6 +4,7 @@ use PhpSchool\CliMenu\CliMenu; use PhpSchool\CliMenu\Exception\MenuNotOpenException; +use PhpSchool\CliMenu\IO\BufferedOutput; use PhpSchool\CliMenu\MenuItem\LineBreakItem; use PhpSchool\CliMenu\MenuItem\SelectableItem; use PhpSchool\CliMenu\MenuStyle; @@ -16,6 +17,33 @@ */ class CliMenuTest extends TestCase { + /** + * @var TerminalInterface + */ + private $terminal; + + /** + * @var BufferedOutput + */ + private $output; + + public function setUp() + { + $this->output = new BufferedOutput; + $this->terminal = $this->createMock(TerminalInterface::class); + $this->terminal->expects($this->any()) + ->method('getOutput') + ->willReturn($this->output); + + $this->terminal->expects($this->any()) + ->method('isTTY') + ->willReturn(true); + + $this->terminal->expects($this->any()) + ->method('getWidth') + ->willReturn(50); + } + public function testGetMenuStyle() : void { $menu = new CliMenu('PHP School FTW', []); @@ -37,49 +65,29 @@ public function testReDrawThrowsExceptionIfMenuNotOpen() : void public function testSimpleOpenClose() : void { - $terminal = $this->createMock(TerminalInterface::class); - - $terminal->expects($this->any()) - ->method('isTTY') - ->willReturn(true); - - $terminal->expects($this->once()) + $this->terminal->expects($this->once()) ->method('getKeyedInput') ->willReturn('enter'); - $terminal->expects($this->any()) - ->method('getWidth') - ->willReturn(50); - - $style = $this->getStyle($terminal); + $style = $this->getStyle($this->terminal); $item = new SelectableItem('Item 1', function (CliMenu $menu) { $menu->close(); }); - $this->expectOutputString(file_get_contents($this->getTestFile())); - - $menu = new CliMenu('PHP School FTW', [$item], $terminal, $style); + $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); + + static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); } public function testReDrawReDrawsImmediately() : void { - $terminal = $this->createMock(TerminalInterface::class); - - $terminal->expects($this->any()) - ->method('isTTY') - ->willReturn(true); - - $terminal->expects($this->once()) + $this->terminal->expects($this->once()) ->method('getKeyedInput') ->willReturn('enter'); - $terminal->expects($this->any()) - ->method('getWidth') - ->willReturn(50); - - $style = $this->getStyle($terminal); + $style = $this->getStyle($this->terminal); $item = new SelectableItem('Item 1', function (CliMenu $menu) { $menu->getStyle()->setBg('red'); @@ -87,10 +95,10 @@ public function testReDrawReDrawsImmediately() : void $menu->close(); }); - $this->expectOutputString(file_get_contents($this->getTestFile())); - - $menu = new CliMenu('PHP School FTW', [$item], $terminal, $style); + $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); + + static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); } public function testGetItems() : void diff --git a/test/Dialogue/ConfirmTest.php b/test/Dialogue/ConfirmTest.php index 10ae72b0..d97cb6a5 100644 --- a/test/Dialogue/ConfirmTest.php +++ b/test/Dialogue/ConfirmTest.php @@ -3,6 +3,7 @@ namespace PhpSchool\CliMenuTest\Dialogue; use PhpSchool\CliMenu\CliMenu; +use PhpSchool\CliMenu\IO\BufferedOutput; use PhpSchool\CliMenu\MenuItem\SelectableItem; use PhpSchool\CliMenu\MenuStyle; use PhpSchool\CliMenu\Terminal\TerminalInterface; @@ -13,26 +14,43 @@ */ class ConfirmTest extends TestCase { - public function testConfirmWithOddLengthConfirmAndButton() : void + /** + * @var TerminalInterface + */ + private $terminal; + + /** + * @var BufferedOutput + */ + private $output; + + public function setUp() { - $terminal = $this->createMock(TerminalInterface::class); + $this->output = new BufferedOutput; + $this->terminal = $this->createMock(TerminalInterface::class); + $this->terminal->expects($this->any()) + ->method('getOutput') + ->willReturn($this->output); - $terminal->expects($this->any()) + $this->terminal->expects($this->any()) ->method('isTTY') ->willReturn(true); - $terminal + $this->terminal->expects($this->any()) + ->method('getWidth') + ->willReturn(50); + } + + public function testConfirmWithOddLengthConfirmAndButton() : void + { + $this->terminal ->method('getKeyedInput') ->will($this->onConsecutiveCalls( 'enter', 'enter' )); - $terminal->expects($this->any()) - ->method('getWidth') - ->willReturn(50); - - $style = $this->getStyle($terminal); + $style = $this->getStyle($this->terminal); $item = new SelectableItem('Item 1', function (CliMenu $menu) { $menu->confirm('PHP School FTW!') @@ -41,32 +59,22 @@ public function testConfirmWithOddLengthConfirmAndButton() : void $menu->close(); }); - $this->expectOutputString(file_get_contents($this->getTestFile())); - - $menu = new CliMenu('PHP School FTW', [$item], $terminal, $style); + $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); + + static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); } public function testConfirmWithEvenLengthConfirmAndButton() : void { - $terminal = $this->createMock(TerminalInterface::class); - - $terminal->expects($this->any()) - ->method('isTTY') - ->willReturn(true); - - $terminal + $this->terminal ->method('getKeyedInput') ->will($this->onConsecutiveCalls( 'enter', 'enter' )); - $terminal->expects($this->any()) - ->method('getWidth') - ->willReturn(50); - - $style = $this->getStyle($terminal); + $style = $this->getStyle($this->terminal); $item = new SelectableItem('Item 1', function (CliMenu $menu) { $menu->confirm('PHP School FTW') @@ -75,32 +83,22 @@ public function testConfirmWithEvenLengthConfirmAndButton() : void $menu->close(); }); - $this->expectOutputString(file_get_contents($this->getTestFile())); - - $menu = new CliMenu('PHP School FTW', [$item], $terminal, $style); + $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); + + static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); } public function testConfirmWithEvenLengthConfirmAndOddLengthButton() : void { - $terminal = $this->createMock(TerminalInterface::class); - - $terminal->expects($this->any()) - ->method('isTTY') - ->willReturn(true); - - $terminal + $this->terminal ->method('getKeyedInput') ->will($this->onConsecutiveCalls( 'enter', 'enter' )); - $terminal->expects($this->any()) - ->method('getWidth') - ->willReturn(50); - - $style = $this->getStyle($terminal); + $style = $this->getStyle($this->terminal); $item = new SelectableItem('Item 1', function (CliMenu $menu) { $menu->confirm('PHP School FTW') @@ -109,32 +107,22 @@ public function testConfirmWithEvenLengthConfirmAndOddLengthButton() : void $menu->close(); }); - $this->expectOutputString(file_get_contents($this->getTestFile())); - - $menu = new CliMenu('PHP School FTW', [$item], $terminal, $style); + $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); + + static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); } public function testConfirmWithOddLengthConfirmAndEvenLengthButton() : void { - $terminal = $this->createMock(TerminalInterface::class); - - $terminal->expects($this->any()) - ->method('isTTY') - ->willReturn(true); - - $terminal + $this->terminal ->method('getKeyedInput') ->will($this->onConsecutiveCalls( 'enter', 'enter' )); - $terminal->expects($this->any()) - ->method('getWidth') - ->willReturn(50); - - $style = $this->getStyle($terminal); + $style = $this->getStyle($this->terminal); $item = new SelectableItem('Item 1', function (CliMenu $menu) { $menu->confirm('PHP School FTW!') @@ -143,21 +131,15 @@ public function testConfirmWithOddLengthConfirmAndEvenLengthButton() : void $menu->close(); }); - $this->expectOutputString(file_get_contents($this->getTestFile())); - - $menu = new CliMenu('PHP School FTW', [$item], $terminal, $style); + $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); + + static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); } public function testConfirmCanOnlyBeClosedWithEnter() : void { - $terminal = $this->createMock(TerminalInterface::class); - - $terminal->expects($this->any()) - ->method('isTTY') - ->willReturn(true); - - $terminal + $this->terminal ->method('getKeyedInput') ->will($this->onConsecutiveCalls( 'enter', @@ -166,11 +148,7 @@ public function testConfirmCanOnlyBeClosedWithEnter() : void 'enter' )); - $terminal->expects($this->any()) - ->method('getWidth') - ->willReturn(50); - - $style = $this->getStyle($terminal); + $style = $this->getStyle($this->terminal); $item = new SelectableItem('Item 1', function (CliMenu $menu) { $menu->confirm('PHP School FTW!') @@ -179,10 +157,10 @@ public function testConfirmCanOnlyBeClosedWithEnter() : void $menu->close(); }); - $this->expectOutputString(file_get_contents($this->getTestFile())); - - $menu = new CliMenu('PHP School FTW', [$item], $terminal, $style); + $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); + + static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); } private function getTestFile() : string diff --git a/test/Dialogue/FlashTest.php b/test/Dialogue/FlashTest.php index dced92ce..09869ebe 100644 --- a/test/Dialogue/FlashTest.php +++ b/test/Dialogue/FlashTest.php @@ -3,6 +3,7 @@ namespace PhpSchool\CliMenuTest\Dialogue; use PhpSchool\CliMenu\CliMenu; +use PhpSchool\CliMenu\IO\BufferedOutput; use PhpSchool\CliMenu\MenuItem\SelectableItem; use PhpSchool\CliMenu\MenuStyle; use PhpSchool\CliMenu\Terminal\TerminalInterface; @@ -13,26 +14,43 @@ */ class FlashTest extends TestCase { - public function testFlashWithOddLength() : void + /** + * @var TerminalInterface + */ + private $terminal; + + /** + * @var BufferedOutput + */ + private $output; + + public function setUp() { - $terminal = $this->createMock(TerminalInterface::class); + $this->output = new BufferedOutput; + $this->terminal = $this->createMock(TerminalInterface::class); + $this->terminal->expects($this->any()) + ->method('getOutput') + ->willReturn($this->output); - $terminal->expects($this->any()) + $this->terminal->expects($this->any()) ->method('isTTY') ->willReturn(true); - $terminal + $this->terminal->expects($this->any()) + ->method('getWidth') + ->willReturn(50); + } + + public function testFlashWithOddLength() : void + { + $this->terminal ->method('getKeyedInput') ->will($this->onConsecutiveCalls( 'enter', 'enter' )); - $terminal->expects($this->any()) - ->method('getWidth') - ->willReturn(50); - - $style = $this->getStyle($terminal); + $style = $this->getStyle($this->terminal); $item = new SelectableItem('Item 1', function (CliMenu $menu) { $menu->flash('PHP School FTW!') @@ -41,32 +59,22 @@ public function testFlashWithOddLength() : void $menu->close(); }); - $this->expectOutputString(file_get_contents($this->getTestFile())); - - $menu = new CliMenu('PHP School FTW', [$item], $terminal, $style); + $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); + + static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); } public function testFlashWithEvenLength() : void { - $terminal = $this->createMock(TerminalInterface::class); - - $terminal->expects($this->any()) - ->method('isTTY') - ->willReturn(true); - - $terminal + $this->terminal ->method('getKeyedInput') ->will($this->onConsecutiveCalls( 'enter', 'enter' )); - $terminal->expects($this->any()) - ->method('getWidth') - ->willReturn(50); - - $style = $this->getStyle($terminal); + $style = $this->getStyle($this->terminal); $item = new SelectableItem('Item 1', function (CliMenu $menu) { $menu->flash('PHP School FTW') @@ -75,10 +83,10 @@ public function testFlashWithEvenLength() : void $menu->close(); }); - $this->expectOutputString(file_get_contents($this->getTestFile())); - - $menu = new CliMenu('PHP School FTW', [$item], $terminal, $style); + $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); + + static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); } /** @@ -86,21 +94,11 @@ public function testFlashWithEvenLength() : void */ public function testFlashCanBeClosedWithAnyKey(string $key) : void { - $terminal = $this->createMock(TerminalInterface::class); - - $terminal->expects($this->any()) - ->method('isTTY') - ->willReturn(true); - - $terminal + $this->terminal ->method('getKeyedInput') ->will($this->onConsecutiveCalls('enter', $key)); - $terminal->expects($this->any()) - ->method('getWidth') - ->willReturn(50); - - $style = $this->getStyle($terminal); + $style = $this->getStyle($this->terminal); $item = new SelectableItem('Item 1', function (CliMenu $menu) { $menu->flash('PHP School FTW!') @@ -109,10 +107,10 @@ public function testFlashCanBeClosedWithAnyKey(string $key) : void $menu->close(); }); - $this->expectOutputString(file_get_contents($this->getTestFile())); - - $menu = new CliMenu('PHP School FTW', [$item], $terminal, $style); + $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); + + static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); } public function keyProvider() : array diff --git a/test/IO/BufferedOutputTest.php b/test/IO/BufferedOutputTest.php new file mode 100644 index 00000000..68d1a81c --- /dev/null +++ b/test/IO/BufferedOutputTest.php @@ -0,0 +1,50 @@ + + */ +class BufferedOutputTest extends TestCase +{ + public function testFetch() : void + { + $output = new BufferedOutput; + $output->write('one'); + + static::assertEquals('one', $output->fetch()); + } + + public function testFetchWithMultipleWrites() : void + { + $output = new BufferedOutput; + $output->write('one'); + $output->write('two'); + + static::assertEquals('onetwo', $output->fetch()); + } + + public function testFetchCleansBufferByDefault() : void + { + $output = new BufferedOutput; + $output->write('one'); + + static::assertEquals('one', $output->fetch()); + static::assertEquals('', $output->fetch()); + } + + public function testFetchWithoutCleaning() : void + { + $output = new BufferedOutput; + $output->write('one'); + + static::assertEquals('one', $output->fetch(false)); + + $output->write('two'); + + static::assertEquals('onetwo', $output->fetch(false)); + } +} diff --git a/test/IO/ResourceInputStreamTest.php b/test/IO/ResourceInputStreamTest.php new file mode 100644 index 00000000..da01a7c3 --- /dev/null +++ b/test/IO/ResourceInputStreamTest.php @@ -0,0 +1,44 @@ + + */ +class ResourceInputStreamTest extends TestCase +{ + public function testNonStream() : void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Expected a valid stream'); + new ResourceInputStream(42); + } + + public function testNotReadable() : void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Expected a readable stream'); + new ResourceInputStream(\STDOUT); + } + + public function testRead() : void + { + $stream = fopen('php://memory','r+'); + fwrite($stream, '1234'); + rewind($stream); + + $inputStream = new ResourceInputStream($stream); + + $input = ''; + $inputStream->read(4, function ($buffer) use (&$input) { + $input .= $buffer; + }); + + static::assertSame('1234', $input); + + fclose($stream); + } +} diff --git a/test/IO/ResourceOutputStreamTest.php b/test/IO/ResourceOutputStreamTest.php new file mode 100644 index 00000000..d7c296c0 --- /dev/null +++ b/test/IO/ResourceOutputStreamTest.php @@ -0,0 +1,36 @@ + + */ +class ResourceOutputStreamTest extends TestCase +{ + public function testNonStream() : void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Expected a valid stream'); + new ResourceOutputStream(42); + } + + public function testNotWritable() : void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Expected a writable stream'); + new ResourceOutputStream(\STDIN); + } + + public function testWrite() : void + { + $stream = fopen('php://memory','r+'); + $outputStream = new ResourceOutputStream($stream); + $outputStream->write('123456789'); + + rewind($stream); + static::assertEquals('123456789', stream_get_contents($stream)); + } +} From 8e04ad1d72d6170f49caa7e9f5e59a0993f785bf Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Mon, 16 Apr 2018 21:32:33 +0200 Subject: [PATCH 02/16] CS --- src/Terminal/TerminalInterface.php | 1 + src/Terminal/UnixTerminal.php | 1 + test/IO/ResourceInputStreamTest.php | 2 +- test/IO/ResourceOutputStreamTest.php | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Terminal/TerminalInterface.php b/src/Terminal/TerminalInterface.php index 1ad18ad4..31f676e4 100644 --- a/src/Terminal/TerminalInterface.php +++ b/src/Terminal/TerminalInterface.php @@ -1,6 +1,7 @@ write('123456789'); From f79a18e59863d37dce4064dae817cfc215601509 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Sun, 15 Apr 2018 22:58:49 +0200 Subject: [PATCH 03/16] Number, Text and Password inputs --- examples/input-advanced.php | 32 +++++ examples/input-number.php | 24 ++++ examples/input-password.php | 24 ++++ examples/input-text.php | 24 ++++ src/CliMenu.php | 47 +++++- src/Input/Input.php | 27 ++++ src/Input/InputIO.php | 260 ++++++++++++++++++++++++++++++++++ src/Input/InputResult.php | 24 ++++ src/Input/Number.php | 96 +++++++++++++ src/Input/Password.php | 100 +++++++++++++ src/Input/Text.php | 85 +++++++++++ src/Terminal/UnixTerminal.php | 29 ++-- 12 files changed, 755 insertions(+), 17 deletions(-) create mode 100644 examples/input-advanced.php create mode 100644 examples/input-number.php create mode 100644 examples/input-password.php create mode 100644 examples/input-text.php create mode 100644 src/Input/Input.php create mode 100644 src/Input/InputIO.php create mode 100644 src/Input/InputResult.php create mode 100644 src/Input/Number.php create mode 100644 src/Input/Password.php create mode 100644 src/Input/Text.php diff --git a/examples/input-advanced.php b/examples/input-advanced.php new file mode 100644 index 00000000..61458bd9 --- /dev/null +++ b/examples/input-advanced.php @@ -0,0 +1,32 @@ +askText() + ->setPromptText('Enter username') + ->setPlaceholderText('alice') + ->ask(); + + $age = $menu->askNumber() + ->setPromptText('Enter age') + ->setPlaceholderText('28') + ->ask(); + + $password = $menu->askPassword() + ->setPromptText('Enter password') + ->ask(); + + var_dump($username->fetch(), $age->fetch(), $password->fetch()); +}; + +$menu = (new CliMenuBuilder) + ->setTitle('User Manager') + ->addItem('Create New User', $itemCallable) + ->addLineBreak('-') + ->build(); + +$menu->open(); diff --git a/examples/input-number.php b/examples/input-number.php new file mode 100644 index 00000000..ea8fa902 --- /dev/null +++ b/examples/input-number.php @@ -0,0 +1,24 @@ +askNumber() + ->setPlaceholderText(10) + ->ask(); + + var_dump($result->fetch()); +}; + +$menu = (new CliMenuBuilder) + ->setTitle('Basic CLI Menu') + ->addItem('Enter number', $itemCallable) + ->addItem('Second Item', $itemCallable) + ->addItem('Third Item', $itemCallable) + ->addLineBreak('-') + ->build(); + +$menu->open(); diff --git a/examples/input-password.php b/examples/input-password.php new file mode 100644 index 00000000..c7d4406f --- /dev/null +++ b/examples/input-password.php @@ -0,0 +1,24 @@ +askPassword() + ->setPlaceholderText('') + ->ask(); + + var_dump($result->fetch()); +}; + +$menu = (new CliMenuBuilder) + ->setTitle('Basic CLI Menu') + ->addItem('Enter password', $itemCallable) + ->addItem('Second Item', $itemCallable) + ->addItem('Third Item', $itemCallable) + ->addLineBreak('-') + ->build(); + +$menu->open(); diff --git a/examples/input-text.php b/examples/input-text.php new file mode 100644 index 00000000..01b3b731 --- /dev/null +++ b/examples/input-text.php @@ -0,0 +1,24 @@ +askText() + ->setPlaceholderText('Enter something here') + ->ask(); + + var_dump($result->fetch()); +}; + +$menu = (new CliMenuBuilder) + ->setTitle('Basic CLI Menu') + ->addItem('Enter text', $itemCallable) + ->addItem('Second Item', $itemCallable) + ->addItem('Third Item', $itemCallable) + ->addLineBreak('-') + ->build(); + +$menu->open(); diff --git a/src/CliMenu.php b/src/CliMenu.php index e3dad919..461b318c 100644 --- a/src/CliMenu.php +++ b/src/CliMenu.php @@ -2,9 +2,14 @@ namespace PhpSchool\CliMenu; +use PhpSchool\CliMenu\Dialogue\NumberInput; use PhpSchool\CliMenu\Exception\InvalidInstantiationException; use PhpSchool\CliMenu\Exception\InvalidTerminalException; use PhpSchool\CliMenu\Exception\MenuNotOpenException; +use PhpSchool\CliMenu\Input\InputIO; +use PhpSchool\CliMenu\Input\Number; +use PhpSchool\CliMenu\Input\Password; +use PhpSchool\CliMenu\Input\Text; use PhpSchool\CliMenu\MenuItem\LineBreakItem; use PhpSchool\CliMenu\MenuItem\MenuItemInterface; use PhpSchool\CliMenu\MenuItem\StaticItem; @@ -359,9 +364,7 @@ public function getCurrentFrame() : Frame public function flash(string $text) : Flash { - if (strpos($text, "\n") !== false) { - throw new \InvalidArgumentException; - } + $this->guardSingleLine($text); $style = (new MenuStyle($this->terminal)) ->setBg('yellow') @@ -372,9 +375,7 @@ public function flash(string $text) : Flash public function confirm($text) : Confirm { - if (strpos($text, "\n") !== false) { - throw new \InvalidArgumentException; - } + $this->guardSingleLine($text); $style = (new MenuStyle($this->terminal)) ->setBg('yellow') @@ -382,4 +383,38 @@ public function confirm($text) : Confirm return new Confirm($this, $style, $this->terminal, $text); } + + public function askNumber() : Number + { + $style = (new MenuStyle($this->terminal)) + ->setBg('yellow') + ->setFg('red'); + + return new Number(new InputIO($this, $style, $this->terminal)); + } + + public function askText() : Text + { + $style = (new MenuStyle($this->terminal)) + ->setBg('yellow') + ->setFg('red'); + + return new Text(new InputIO($this, $style, $this->terminal)); + } + + public function askPassword() : Password + { + $style = (new MenuStyle($this->terminal)) + ->setBg('yellow') + ->setFg('red'); + + return new Password(new InputIO($this, $style, $this->terminal)); + } + + private function guardSingleLine($text) + { + if (strpos($text, "\n") !== false) { + throw new \InvalidArgumentException; + } + } } diff --git a/src/Input/Input.php b/src/Input/Input.php new file mode 100644 index 00000000..95fccbbd --- /dev/null +++ b/src/Input/Input.php @@ -0,0 +1,27 @@ + + */ +interface Input +{ + public function ask() : InputResult; + + public function validate(string $input) : bool; + + public function setPromptText(string $promptText) : Input; + + public function getPromptText() : string; + + public function setValidationFailedText(string $validationFailedText) : Input; + + public function getValidationFailedText() : string; + + public function setPlaceholderText(string $placeholderText) : Input; + + public function getPlaceholderText() : string; + + public function format(string $value) : string; +} diff --git a/src/Input/InputIO.php b/src/Input/InputIO.php new file mode 100644 index 00000000..b8b3375a --- /dev/null +++ b/src/Input/InputIO.php @@ -0,0 +1,260 @@ + + */ +class InputIO +{ + /** + * @var MenuStyle + */ + private $style; + + /** + * @var CliMenu + */ + private $parentMenu; + + /** + * @var TerminalInterface + */ + private $terminal; + + /** + * @var array + */ + private $inputMap = [ + "\n" => 'enter', + "\r" => 'enter', + "\177" => 'backspace' + ]; + + /** + * @var callable[][] + */ + private $callbacks = []; + + public function __construct(CliMenu $parentMenu, MenuStyle $menuStyle, TerminalInterface $terminal) + { + $this->style = $menuStyle; + $this->terminal = $terminal; + $this->parentMenu = $parentMenu; + } + + public function collect(Input $input) : InputResult + { + $this->drawInput($input, $input->getPlaceholderText()); + + $inputValue = $input->getPlaceholderText(); + + while (($userInput = $this->terminal->getKeyedInput($this->inputMap)) !== false) { + $this->parentMenu->redraw(); + $this->drawInput($input, $inputValue); + + if ($userInput === 'enter') { + if ($input->validate($inputValue)) { + $this->parentMenu->redraw(); + return new InputResult($inputValue); + } else { + $this->drawInputWithError($input, $inputValue); + continue; + } + } + + if ($userInput === 'backspace') { + $inputValue = substr($inputValue, 0, -1); + $this->drawInput($input, $inputValue); + continue; + } + + if (!empty($this->callbacks[$userInput])) { + foreach ($this->callbacks[$userInput] as $callback) { + $inputValue = $callback($this, $inputValue); + $this->drawInput($input, $inputValue); + } + continue; + } + + $inputValue .= $userInput; + $this->drawInput($input, $inputValue); + } + } + + public function registerInputMap(string $input, string $mapTo) : void + { + $this->inputMap[$input] = $mapTo; + } + + public function registerControlCallback(string $control, callable $callback) : void + { + if (!isset($this->callbacks[$control])) { + $this->callbacks[$control] = []; + } + + $this->callbacks[$control][] = $callback; + } + + private function getInputWidth(array $lines) + { + return max( + array_map( + function (string $line) { + return mb_strlen($line); + }, + $lines + ) + ); + } + + private function calculateYPosition(Input $input) : int + { + $lines = 5; //1. empty 2. prompt text 3. empty 4. input 5. empty + + return ceil($this->parentMenu->getCurrentFrame()->count() / 2) - ceil($lines /2) + 1; + } + + private function calculateYPositionWithError() : int + { + $lines = 7; //1. empty 2. prompt text 3. empty 4. input 5. empty 6. error 7. empty + + return ceil($this->parentMenu->getCurrentFrame()->count() / 2) - ceil($lines /2) + 1; + } + + private function calculateXPosition(Input $input, string $userInput) : int + { + $width = $this->getInputWidth( + [ + $input->getPromptText(), + $input->getValidationFailedText(), + $userInput + ] + ); + + $parentStyle = $this->parentMenu->getStyle(); + $halfWidth = ($width + ($this->style->getPadding() * 2)) / 2; + $parentHalfWidth = ceil($parentStyle->getWidth() / 2); + + return $parentHalfWidth - $halfWidth; + } + + private function drawLine(Input $input, string $userInput, string $text) : void + { + $this->terminal->moveCursorToColumn($this->calculateXPosition($input, $userInput)); + + printf( + "%s%s%s%s%s\n", + $this->style->getUnselectedSetCode(), + str_repeat(' ', $this->style->getPadding()), + $text, + str_repeat(' ', $this->style->getPadding()), + $this->style->getUnselectedUnsetCode() + ); + } + + private function drawCenteredLine(Input $input, string $userInput, string $text) : void + { + $width = $this->getInputWidth( + [ + $input->getPromptText(), + $input->getValidationFailedText(), + $userInput + ] + ); + + $textLength = mb_strlen(StringUtil::stripAnsiEscapeSequence($text)); + $leftFill = ($width / 2) - ($textLength / 2); + $rightFill = ceil($width - $leftFill - $textLength); + + $this->drawLine( + $input, + $userInput, + sprintf( + '%s%s%s', + str_repeat(' ', $leftFill), + $text, + str_repeat(' ', $rightFill) + ) + ); + } + + private function drawEmptyLine(Input $input, string $userInput) : void + { + $width = $this->getInputWidth( + [ + $input->getPromptText(), + $input->getValidationFailedText(), + $userInput + ] + ); + + $this->drawLine( + $input, + $userInput, + str_repeat(' ', $width) + ); + } + + private function drawInput(Input $input, string $userInput) : void + { + $this->terminal->moveCursorToRow($this->calculateYPosition($input)); + + $this->drawEmptyLine($input, $userInput); + $this->drawTitle($input, $userInput); + $this->drawEmptyLine($input, $userInput); + $this->drawInputField($input, $input->format($userInput)); + $this->drawEmptyLine($input, $userInput); + } + + private function drawInputWithError(Input $input, string $userInput) : void + { + $this->terminal->moveCursorToRow($this->calculateYPositionWithError($input)); + + $this->drawEmptyLine($input, $userInput); + $this->drawTitle($input, $userInput); + $this->drawEmptyLine($input, $userInput); + $this->drawInputField($input, $input->format($userInput)); + $this->drawEmptyLine($input, $userInput); + $this->drawCenteredLine( + $input, + $userInput, + sprintf( + '%s', + $input->getValidationFailedText() + ) + ); + $this->drawEmptyLine($input, $userInput); + } + + private function drawTitle(Input $input, string $userInput) : void + { + + $this->drawCenteredLine( + $input, + $userInput, + $input->getPromptText() + ); + } + + private function drawInputField(Input $input, string $userInput) : void + { + $this->drawCenteredLine( + $input, + $userInput, + sprintf( + '%s%s%s%s%s', + $this->style->getUnselectedUnsetCode(), + $this->style->getSelectedSetCode(), + $userInput, + $this->style->getSelectedUnsetCode(), + $this->style->getUnselectedSetCode() + ) + ); + } +} diff --git a/src/Input/InputResult.php b/src/Input/InputResult.php new file mode 100644 index 00000000..43f526a3 --- /dev/null +++ b/src/Input/InputResult.php @@ -0,0 +1,24 @@ + + */ +class InputResult +{ + /** + * @var string + */ + private $input; + + public function __construct(string $input) + { + $this->input = $input; + } + + public function fetch() : string + { + return $this->input; + } +} diff --git a/src/Input/Number.php b/src/Input/Number.php new file mode 100644 index 00000000..6ca46e32 --- /dev/null +++ b/src/Input/Number.php @@ -0,0 +1,96 @@ + + */ +class Number implements Input +{ + /** + * @var InputIO + */ + private $inputIO; + + /** + * @var string + */ + private $promptText = 'Enter a number:'; + + /** + * @var string + */ + private $validationFailedText = 'Not a valid number, try again'; + + /** + * @var string + */ + private $placeholderText = ''; + + public function __construct(InputIO $inputIO) + { + $this->inputIO = $inputIO; + } + + public function setPromptText(string $promptText) : Input + { + $this->promptText = $promptText; + + return $this; + } + + public function getPromptText() : string + { + return $this->promptText; + } + + public function setValidationFailedText(string $validationFailedText) : Input + { + $this->validationFailedText = $validationFailedText; + + return $this; + } + + public function getValidationFailedText() : string + { + return $this->validationFailedText; + } + + public function setPlaceholderText(string $placeholderText) : Input + { + $this->placeholderText = $placeholderText; + + return $this; + } + + public function getPlaceholderText() : string + { + return $this->placeholderText; + } + + public function ask() : InputResult + { + $this->inputIO->registerInputMap("\033[A", 'up'); + $this->inputIO->registerInputMap("\033[B", 'down'); + + $this->inputIO->registerControlCallback('up', function (InputIO $inputIO, string $input) { + return $this->validate($input) ? $input + 1 : $input; + }); + + $this->inputIO->registerControlCallback('down', function (InputIO $inputIO, string $input) { + return $this->validate($input) ? $input - 1 : $input; + }); + + return $this->inputIO->collect($this); + } + + public function validate(string $input) : bool + { + return (bool) preg_match('/^\d+$/', $input); + } + + public function format(string $value) : string + { + return $value; + } +} diff --git a/src/Input/Password.php b/src/Input/Password.php new file mode 100644 index 00000000..1e72eada --- /dev/null +++ b/src/Input/Password.php @@ -0,0 +1,100 @@ + + */ +class Password implements Input +{ + /** + * @var InputIO + */ + private $inputIO; + + /** + * @var string + */ + private $promptText = 'Enter password:'; + + /** + * @var string + */ + private $validationFailedText = 'Invalid password, try again'; + + /** + * @var string + */ + private $placeholderText = ''; + + /** + * @var null|callable + */ + private $validator; + + public function __construct(InputIO $inputIO) + { + $this->inputIO = $inputIO; + } + + public function setPromptText(string $promptText) : Input + { + $this->promptText = $promptText; + + return $this; + } + + public function getPromptText() : string + { + return $this->promptText; + } + + public function setValidationFailedText(string $validationFailedText) : Input + { + $this->validationFailedText = $validationFailedText; + + return $this; + } + + public function getValidationFailedText() : string + { + return $this->validationFailedText; + } + + public function setPlaceholderText(string $placeholderText) : Input + { + $this->placeholderText = $placeholderText; + + return $this; + } + + public function getPlaceholderText() : string + { + return $this->placeholderText; + } + + public function setValidator(callable $validator) + { + $this->validator = $validator; + } + + public function ask() : InputResult + { + return $this->inputIO->collect($this); + } + + public function validate(string $input) : bool + { + if ($this->validator) { + $validator = $this->validator; + return $validator($input); + } + + return mb_strlen($input) > 16; + } + + public function format(string $value) : string + { + return str_repeat('*', mb_strlen($value)); + } +} diff --git a/src/Input/Text.php b/src/Input/Text.php new file mode 100644 index 00000000..dc01052c --- /dev/null +++ b/src/Input/Text.php @@ -0,0 +1,85 @@ + + */ +class Text implements Input +{ + /** + * @var InputIO + */ + private $inputIO; + + /** + * @var string + */ + private $promptText = 'Enter text:'; + + /** + * @var string + */ + private $validationFailedText = 'Invalid, try again'; + + /** + * @var string + */ + private $placeholderText = ''; + + public function __construct(InputIO $inputIO) + { + $this->inputIO = $inputIO; + } + + public function setPromptText(string $promptText) : Input + { + $this->promptText = $promptText; + + return $this; + } + + public function getPromptText() : string + { + return $this->promptText; + } + + public function setValidationFailedText(string $validationFailedText) : Input + { + $this->validationFailedText = $validationFailedText; + + return $this; + } + + public function getValidationFailedText() : string + { + return $this->validationFailedText; + } + + public function setPlaceholderText(string $placeholderText) : Input + { + $this->placeholderText = $placeholderText; + + return $this; + } + + public function getPlaceholderText() : string + { + return $this->placeholderText; + } + + public function ask() : InputResult + { + return $this->inputIO->collect($this); + } + + public function validate(string $input) : bool + { + return !empty($input); + } + + public function format(string $value) : string + { + return $value; + } +} diff --git a/src/Terminal/UnixTerminal.php b/src/Terminal/UnixTerminal.php index d095e5ce..5b5bb6f8 100644 --- a/src/Terminal/UnixTerminal.php +++ b/src/Terminal/UnixTerminal.php @@ -144,21 +144,28 @@ public function supportsColour() : bool return $this->isTTY(); } + /** + * @param array $map Provide an alternative map + */ public function getKeyedInput(array $map = []) : ?string { // TODO: Move to class var? // TODO: up, down, enter etc in Abstract CONSTs - $map = [ - "\033[A" => 'up', - "k" => 'up', - "" => 'up', // emacs ^P - "\033[B" => 'down', - "j" => 'down', - "" => 'down', //emacs ^N - "\n" => 'enter', - "\r" => 'enter', - " " => 'enter', - ]; + + if (empty($map)) { + $map = [ + "\033[A" => 'up', + "k" => 'up', + "" => 'up', // emacs ^P + "\033[B" => 'down', + "j" => 'down', + "" => 'down', //emacs ^N + "\n" => 'enter', + "\r" => 'enter', + " " => 'enter', + "\177" => 'backspace' + ]; + } $input = ''; $this->input->read(4, function ($buffer) use (&$input) { From 36523029effca4539daafb2fe851cedb6e803c95 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Mon, 16 Apr 2018 14:49:26 +0200 Subject: [PATCH 04/16] Fix some static analysis bugs --- src/Input/InputIO.php | 8 ++++---- src/Terminal/TerminalInterface.php | 2 +- src/Terminal/UnixTerminal.php | 4 ++++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Input/InputIO.php b/src/Input/InputIO.php index b8b3375a..dffb2910 100644 --- a/src/Input/InputIO.php +++ b/src/Input/InputIO.php @@ -54,7 +54,7 @@ public function collect(Input $input) : InputResult $inputValue = $input->getPlaceholderText(); - while (($userInput = $this->terminal->getKeyedInput($this->inputMap)) !== false) { + while (($userInput = $this->terminal->getKeyedInput($this->inputMap)) !== null) { $this->parentMenu->redraw(); $this->drawInput($input, $inputValue); @@ -113,7 +113,7 @@ function (string $line) { ); } - private function calculateYPosition(Input $input) : int + private function calculateYPosition() : int { $lines = 5; //1. empty 2. prompt text 3. empty 4. input 5. empty @@ -203,7 +203,7 @@ private function drawEmptyLine(Input $input, string $userInput) : void private function drawInput(Input $input, string $userInput) : void { - $this->terminal->moveCursorToRow($this->calculateYPosition($input)); + $this->terminal->moveCursorToRow($this->calculateYPosition()); $this->drawEmptyLine($input, $userInput); $this->drawTitle($input, $userInput); @@ -214,7 +214,7 @@ private function drawInput(Input $input, string $userInput) : void private function drawInputWithError(Input $input, string $userInput) : void { - $this->terminal->moveCursorToRow($this->calculateYPositionWithError($input)); + $this->terminal->moveCursorToRow($this->calculateYPositionWithError()); $this->drawEmptyLine($input, $userInput); $this->drawTitle($input, $userInput); diff --git a/src/Terminal/TerminalInterface.php b/src/Terminal/TerminalInterface.php index 31f676e4..5a1a8787 100644 --- a/src/Terminal/TerminalInterface.php +++ b/src/Terminal/TerminalInterface.php @@ -85,7 +85,7 @@ public function enableCursor() : void; public function disableCursor() : void; /** - * @return string + * Read input from the terminal */ public function getKeyedInput(array $map = []) : ?string; diff --git a/src/Terminal/UnixTerminal.php b/src/Terminal/UnixTerminal.php index 5b5bb6f8..4fafb68c 100644 --- a/src/Terminal/UnixTerminal.php +++ b/src/Terminal/UnixTerminal.php @@ -174,6 +174,10 @@ public function getKeyedInput(array $map = []) : ?string $this->clearLine(); + if ($input === false) { + return null; + } + return array_key_exists($input, $map) ? $map[$input] : $input; From 06f04c92ba6da86def532b5dfa9b3d346dac86b4 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Mon, 16 Apr 2018 17:26:16 +0200 Subject: [PATCH 05/16] Unit tests for inputs --- src/Input/Input.php | 2 +- src/Input/InputIO.php | 13 ++-- src/Input/Number.php | 9 +-- src/Input/Password.php | 4 +- src/Input/Text.php | 2 +- test/Input/InputIOTest.php | 104 ++++++++++++++++++++++++++++ test/Input/InputResultTest.php | 17 +++++ test/Input/NumberTest.php | 110 +++++++++++++++++++++++++++++ test/Input/PasswordTest.php | 122 +++++++++++++++++++++++++++++++++ test/Input/TextTest.php | 97 ++++++++++++++++++++++++++ 10 files changed, 462 insertions(+), 18 deletions(-) create mode 100644 test/Input/InputIOTest.php create mode 100644 test/Input/InputResultTest.php create mode 100644 test/Input/NumberTest.php create mode 100644 test/Input/PasswordTest.php create mode 100644 test/Input/TextTest.php diff --git a/src/Input/Input.php b/src/Input/Input.php index 95fccbbd..089d7ec9 100644 --- a/src/Input/Input.php +++ b/src/Input/Input.php @@ -23,5 +23,5 @@ public function setPlaceholderText(string $placeholderText) : Input; public function getPlaceholderText() : string; - public function format(string $value) : string; + public function filter(string $value) : string; } diff --git a/src/Input/InputIO.php b/src/Input/InputIO.php index dffb2910..a9811ea5 100644 --- a/src/Input/InputIO.php +++ b/src/Input/InputIO.php @@ -76,7 +76,7 @@ public function collect(Input $input) : InputResult if (!empty($this->callbacks[$userInput])) { foreach ($this->callbacks[$userInput] as $callback) { - $inputValue = $callback($this, $inputValue); + $inputValue = $callback($inputValue); $this->drawInput($input, $inputValue); } continue; @@ -87,13 +87,10 @@ public function collect(Input $input) : InputResult } } - public function registerInputMap(string $input, string $mapTo) : void - { - $this->inputMap[$input] = $mapTo; - } - public function registerControlCallback(string $control, callable $callback) : void { + $this->inputMap[$control] = $control; + if (!isset($this->callbacks[$control])) { $this->callbacks[$control] = []; } @@ -208,7 +205,7 @@ private function drawInput(Input $input, string $userInput) : void $this->drawEmptyLine($input, $userInput); $this->drawTitle($input, $userInput); $this->drawEmptyLine($input, $userInput); - $this->drawInputField($input, $input->format($userInput)); + $this->drawInputField($input, $input->filter($userInput)); $this->drawEmptyLine($input, $userInput); } @@ -219,7 +216,7 @@ private function drawInputWithError(Input $input, string $userInput) : void $this->drawEmptyLine($input, $userInput); $this->drawTitle($input, $userInput); $this->drawEmptyLine($input, $userInput); - $this->drawInputField($input, $input->format($userInput)); + $this->drawInputField($input, $input->filter($userInput)); $this->drawEmptyLine($input, $userInput); $this->drawCenteredLine( $input, diff --git a/src/Input/Number.php b/src/Input/Number.php index 6ca46e32..a37ce9c0 100644 --- a/src/Input/Number.php +++ b/src/Input/Number.php @@ -70,14 +70,11 @@ public function getPlaceholderText() : string public function ask() : InputResult { - $this->inputIO->registerInputMap("\033[A", 'up'); - $this->inputIO->registerInputMap("\033[B", 'down'); - - $this->inputIO->registerControlCallback('up', function (InputIO $inputIO, string $input) { + $this->inputIO->registerControlCallback("\033[A", function (string $input) { return $this->validate($input) ? $input + 1 : $input; }); - $this->inputIO->registerControlCallback('down', function (InputIO $inputIO, string $input) { + $this->inputIO->registerControlCallback("\033[B", function (string $input) { return $this->validate($input) ? $input - 1 : $input; }); @@ -89,7 +86,7 @@ public function validate(string $input) : bool return (bool) preg_match('/^\d+$/', $input); } - public function format(string $value) : string + public function filter(string $value) : string { return $value; } diff --git a/src/Input/Password.php b/src/Input/Password.php index 1e72eada..a52ffdd6 100644 --- a/src/Input/Password.php +++ b/src/Input/Password.php @@ -90,10 +90,10 @@ public function validate(string $input) : bool return $validator($input); } - return mb_strlen($input) > 16; + return mb_strlen($input) >= 16; } - public function format(string $value) : string + public function filter(string $value) : string { return str_repeat('*', mb_strlen($value)); } diff --git a/src/Input/Text.php b/src/Input/Text.php index dc01052c..58a38f90 100644 --- a/src/Input/Text.php +++ b/src/Input/Text.php @@ -78,7 +78,7 @@ public function validate(string $input) : bool return !empty($input); } - public function format(string $value) : string + public function filter(string $value) : string { return $value; } diff --git a/test/Input/InputIOTest.php b/test/Input/InputIOTest.php new file mode 100644 index 00000000..43f85785 --- /dev/null +++ b/test/Input/InputIOTest.php @@ -0,0 +1,104 @@ + + */ +class InputIOTest extends TestCase +{ + /** + * @var TerminalInterface + */ + private $terminal; + + /** + * @var CliMenu + */ + private $menu; + + /** + * @var MenuStyle + */ + private $style; + + /** + * @var InputIO + */ + private $inputIO; + + public function setUp() + { + $this->terminal = $this->createMock(TerminalInterface::class); + $this->menu = $this->createMock(CliMenu::class); + $this->style = $this->createMock(MenuStyle::class); + $this->inputIO = new InputIO($this->menu, $this->style, $this->terminal); + } + + public function testEnterReturnsOutputIfValid() : void + { + $this->terminal + ->expects($this->exactly(2)) + ->method('getKeyedInput') + ->willReturn('1', 'enter'); + + $result = $this->inputIO->collect(new Text($this->inputIO)); + + self::assertEquals('1', $result->fetch()); + } + + public function testCustomControlFunctions() : void + { + $this->inputIO->registerControlCallback('u', function ($input) { + return ++$input; + }); + + $this->terminal + ->expects($this->exactly(4)) + ->method('getKeyedInput') + ->with(["\n" => 'enter', "\r" => 'enter', "\177" => 'backspace', 'u' => 'u']) + ->willReturn('1', '0', 'u', 'enter'); + + $result = $this->inputIO->collect(new Text($this->inputIO)); + + self::assertEquals('11', $result->fetch()); + } + + public function testBackspaceDeletesPreviousCharacter() : void + { + $this->terminal + ->expects($this->exactly(6)) + ->method('getKeyedInput') + ->willReturn('1', '6', '7', 'backspace', 'backspace', 'enter'); + + $result = $this->inputIO->collect(new Text($this->inputIO)); + + self::assertEquals('1', $result->fetch()); + } + + public function testValidationErrorCausesErrorMessageToBeDisplayed() : void + { + $input = new class ($this->inputIO) extends Text { + public function validate(string $input) : bool + { + return $input[-1] === 'p'; + } + }; + + $this->terminal + ->expects($this->exactly(6)) + ->method('getKeyedInput') + ->willReturn('1', 't', 'enter', 'backspace', 'p', 'enter'); + + $result = $this->inputIO->collect($input); + + self::assertEquals('1p', $result->fetch()); + } +} diff --git a/test/Input/InputResultTest.php b/test/Input/InputResultTest.php new file mode 100644 index 00000000..5838421d --- /dev/null +++ b/test/Input/InputResultTest.php @@ -0,0 +1,17 @@ + + */ +class InputResultTest extends TestCase +{ + public function testFetch() : void + { + static::assertEquals('my-password', (new InputResult('my-password'))->fetch()); + } +} diff --git a/test/Input/NumberTest.php b/test/Input/NumberTest.php new file mode 100644 index 00000000..04c5a98a --- /dev/null +++ b/test/Input/NumberTest.php @@ -0,0 +1,110 @@ + + */ +class NumberTest extends TestCase +{ + /** + * @var TerminalInterface + */ + private $terminal; + + /** + * @var InputIO + */ + private $inputIO; + + /** + * @var Number + */ + private $input; + + public function setUp() + { + $this->terminal = $this->createMock(TerminalInterface::class); + $menu = $this->createMock(CliMenu::class); + $style = $this->createMock(MenuStyle::class); + + $this->inputIO = new InputIO($menu, $style, $this->terminal); + $this->input = new Number($this->inputIO); + } + + public function testGetSetPromptText() : void + { + static::assertEquals('Enter a number:', $this->input->getPromptText()); + + $this->input->setPromptText('Number please:'); + static::assertEquals('Number please:', $this->input->getPromptText()); + } + + public function testGetSetValidationFailedText() : void + { + static::assertEquals('Not a valid number, try again', $this->input->getValidationFailedText()); + + $this->input->setValidationFailedText('Failed!'); + static::assertEquals('Failed!', $this->input->getValidationFailedText()); + } + + public function testGetSetPlaceholderText() : void + { + static::assertEquals('', $this->input->getPlaceholderText()); + + $this->input->setPlaceholderText('some placeholder text'); + static::assertEquals('some placeholder text', $this->input->getPlaceholderText()); + } + + /** + * @dataProvider validateProvider + */ + public function testValidate(string $value, bool $result) : void + { + static::assertEquals($this->input->validate($value), $result); + } + + public function validateProvider() : array + { + return [ + ['10', true], + ['10t', false], + ['t10', false], + ['0', true], + ['0000000000', true], + ['9999999999', true], + ]; + } + + public function testFilterReturnsInputAsIs() : void + { + static::assertEquals('9999', $this->input->filter('9999')); + } + + public function testUpKeyIncrementsNumber() : void + { + $this->terminal + ->expects($this->exactly(4)) + ->method('getKeyedInput') + ->willReturn('1', '0', "\033[A", 'enter'); + + self::assertEquals(11, $this->input->ask()->fetch()); + } + + public function testDownKeyDecrementsNumber() : void + { + $this->terminal + ->expects($this->exactly(4)) + ->method('getKeyedInput') + ->willReturn('1', '0', "\033[B", 'enter'); + + self::assertEquals(9, $this->input->ask()->fetch()); + } +} diff --git a/test/Input/PasswordTest.php b/test/Input/PasswordTest.php new file mode 100644 index 00000000..8c9bdab3 --- /dev/null +++ b/test/Input/PasswordTest.php @@ -0,0 +1,122 @@ + + */ +class PasswordTest extends TestCase +{ + /** + * @var TerminalInterface + */ + private $terminal; + + /** + * @var InputIO + */ + private $inputIO; + + /** + * @var Text + */ + private $input; + + public function setUp() + { + $this->terminal = $this->createMock(TerminalInterface::class); + $menu = $this->createMock(CliMenu::class); + $style = $this->createMock(MenuStyle::class); + + $this->inputIO = new InputIO($menu, $style, $this->terminal); + $this->input = new Password($this->inputIO); + } + + public function testGetSetPromptText() : void + { + static::assertEquals('Enter password:', $this->input->getPromptText()); + + $this->input->setPromptText('Password please:'); + static::assertEquals('Password please:', $this->input->getPromptText()); + } + + public function testGetSetValidationFailedText() : void + { + static::assertEquals('Invalid password, try again', $this->input->getValidationFailedText()); + + $this->input->setValidationFailedText('Failed!'); + static::assertEquals('Failed!', $this->input->getValidationFailedText()); + } + + public function testGetSetPlaceholderText() : void + { + static::assertEquals('', $this->input->getPlaceholderText()); + + $this->input->setPlaceholderText('***'); + static::assertEquals('***', $this->input->getPlaceholderText()); + } + + /** + * @dataProvider validateProvider + */ + public function testValidate(string $value, bool $result) : void + { + static::assertEquals($this->input->validate($value), $result); + } + + public function validateProvider() : array + { + return [ + ['10', false], + ['mypassword', false], + ['pppppppppppppppp', true], + ]; + } + + public function testFilterConcealsPassword() : void + { + static::assertEquals('****', $this->input->filter('pppp')); + } + + public function testAskPassword() : void + { + $this->terminal + ->expects($this->exactly(17)) + ->method('getKeyedInput') + ->willReturn('1', '2', '3', '4', '5', '6', '7', '8', '9', '1', '2', '3', '4', '5', '6', '7', 'enter'); + + self::assertEquals('1234567891234567', $this->input->ask()->fetch()); + } + + /** + * @dataProvider customValidateProvider + */ + public function testValidateWithCustomValidator(string $value, bool $result) : void + { + $customValidate = function ($input) { + return preg_match('/\d/', $input) && preg_match('/[a-zA-Z]/', $input); + }; + + $this->input->setValidator($customValidate); + + static::assertEquals($this->input->validate($value), $result); + } + + public function customValidateProvider() : array + { + return [ + ['10', false], + ['mypassword', false], + ['pppppppppppppppp', false], + ['1t', true], + ['999ppp', true], + ]; + } +} diff --git a/test/Input/TextTest.php b/test/Input/TextTest.php new file mode 100644 index 00000000..9b13470a --- /dev/null +++ b/test/Input/TextTest.php @@ -0,0 +1,97 @@ + + */ +class TextTest extends TestCase +{ + /** + * @var TerminalInterface + */ + private $terminal; + + /** + * @var InputIO + */ + private $inputIO; + + /** + * @var Text + */ + private $input; + + public function setUp() + { + $this->terminal = $this->createMock(TerminalInterface::class); + $menu = $this->createMock(CliMenu::class); + $style = $this->createMock(MenuStyle::class); + + $this->inputIO = new InputIO($menu, $style, $this->terminal); + $this->input = new Text($this->inputIO); + } + + public function testGetSetPromptText() : void + { + static::assertEquals('Enter text:', $this->input->getPromptText()); + + $this->input->setPromptText('Text please:'); + static::assertEquals('Text please:', $this->input->getPromptText()); + } + + public function testGetSetValidationFailedText() : void + { + static::assertEquals('Invalid, try again', $this->input->getValidationFailedText()); + + $this->input->setValidationFailedText('Failed!'); + static::assertEquals('Failed!', $this->input->getValidationFailedText()); + } + + public function testGetSetPlaceholderText() : void + { + static::assertEquals('', $this->input->getPlaceholderText()); + + $this->input->setPlaceholderText('My Title'); + static::assertEquals('My Title', $this->input->getPlaceholderText()); + } + + /** + * @dataProvider validateProvider + */ + public function testValidate(string $value, bool $result) : void + { + static::assertEquals($this->input->validate($value), $result); + } + + public function validateProvider() : array + { + return [ + ['', false], + ['some text', true], + ['some more text', true], + ]; + } + + public function testFilterReturnsInputAsIs() : void + { + static::assertEquals('9999', $this->input->filter('9999')); + } + + public function testAskText() : void + { + $this->terminal + ->expects($this->exactly(10)) + ->method('getKeyedInput') + ->willReturn('s', 'o', 'm', 'e', ' ', 't', 'e', 'x', 't', 'enter'); + + self::assertEquals('some text', $this->input->ask()->fetch()); + } +} From 3ac115becd15f74ff8e0bb8470793a9692a14f5d Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Mon, 23 Apr 2018 19:33:25 +0200 Subject: [PATCH 06/16] Remove IO - moved to php-school/terminal --- src/IO/BufferedOutput.php | 32 ------------------------ src/IO/InputStream.php | 15 ------------ src/IO/OutputStream.php | 11 --------- src/IO/ResourceInputStream.php | 43 --------------------------------- src/IO/ResourceOutputStream.php | 42 -------------------------------- 5 files changed, 143 deletions(-) delete mode 100644 src/IO/BufferedOutput.php delete mode 100644 src/IO/InputStream.php delete mode 100644 src/IO/OutputStream.php delete mode 100644 src/IO/ResourceInputStream.php delete mode 100644 src/IO/ResourceOutputStream.php diff --git a/src/IO/BufferedOutput.php b/src/IO/BufferedOutput.php deleted file mode 100644 index 1f07cb2a..00000000 --- a/src/IO/BufferedOutput.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -class BufferedOutput implements OutputStream -{ - private $buffer = ''; - - public function write(string $buffer): void - { - $this->buffer .= $buffer; - } - - public function fetch(bool $clean = true) : string - { - $buffer = $this->buffer; - - if ($clean) { - $this->buffer = ''; - } - - return $buffer; - } - - public function __toString() : string - { - return $this->fetch(); - } -} diff --git a/src/IO/InputStream.php b/src/IO/InputStream.php deleted file mode 100644 index 009f1a09..00000000 --- a/src/IO/InputStream.php +++ /dev/null @@ -1,15 +0,0 @@ - - */ -interface InputStream -{ - /** - * Callback should be called with the number of bytes requested - * when ready. - */ - public function read(int $numBytes, callable $callback) : void; -} diff --git a/src/IO/OutputStream.php b/src/IO/OutputStream.php deleted file mode 100644 index af43e124..00000000 --- a/src/IO/OutputStream.php +++ /dev/null @@ -1,11 +0,0 @@ - - */ -interface OutputStream -{ - public function write(string $buffer) : void; -} diff --git a/src/IO/ResourceInputStream.php b/src/IO/ResourceInputStream.php deleted file mode 100644 index 430da936..00000000 --- a/src/IO/ResourceInputStream.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ -class ResourceInputStream implements InputStream -{ - /** - * @var resource - */ - private $stream; - - public function __construct($stream = null) - { - if ($stream === null) { - $stream = STDIN; - } - - if (!is_resource($stream) || get_resource_type($stream) !== 'stream') { - throw new \InvalidArgumentException('Expected a valid stream'); - } - - $meta = stream_get_meta_data($stream); - if (strpos($meta['mode'], 'r') === false && strpos($meta['mode'], '+') === false) { - throw new \InvalidArgumentException('Expected a readable stream'); - } - - $this->stream = $stream; - } - - public function read(int $numBytes, callable $callback) : void - { - $buffer = fread($this->stream, $numBytes); - $callback($buffer); - } -} diff --git a/src/IO/ResourceOutputStream.php b/src/IO/ResourceOutputStream.php deleted file mode 100644 index 3bef8176..00000000 --- a/src/IO/ResourceOutputStream.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ -class ResourceOutputStream implements OutputStream -{ - /** - * @var resource - */ - private $stream; - - public function __construct($stream = null) - { - if ($stream === null) { - $stream = STDOUT; - } - - if (!is_resource($stream) || get_resource_type($stream) !== 'stream') { - throw new \InvalidArgumentException('Expected a valid stream'); - } - - $meta = stream_get_meta_data($stream); - if (strpos($meta['mode'], 'r') !== false && strpos($meta['mode'], '+') === false) { - throw new \InvalidArgumentException('Expected a writable stream'); - } - - $this->stream = $stream; - } - - public function write(string $buffer): void - { - fwrite($this->stream, $buffer); - } -} From bd8d8a8f8f89dfd3785487cffc2ede98e0eadc7f Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Mon, 23 Apr 2018 19:33:45 +0200 Subject: [PATCH 07/16] Remove IO - moved to php-school/terminal --- test/IO/BufferedOutputTest.php | 50 ---------------------------- test/IO/ResourceInputStreamTest.php | 44 ------------------------ test/IO/ResourceOutputStreamTest.php | 36 -------------------- 3 files changed, 130 deletions(-) delete mode 100644 test/IO/BufferedOutputTest.php delete mode 100644 test/IO/ResourceInputStreamTest.php delete mode 100644 test/IO/ResourceOutputStreamTest.php diff --git a/test/IO/BufferedOutputTest.php b/test/IO/BufferedOutputTest.php deleted file mode 100644 index 68d1a81c..00000000 --- a/test/IO/BufferedOutputTest.php +++ /dev/null @@ -1,50 +0,0 @@ - - */ -class BufferedOutputTest extends TestCase -{ - public function testFetch() : void - { - $output = new BufferedOutput; - $output->write('one'); - - static::assertEquals('one', $output->fetch()); - } - - public function testFetchWithMultipleWrites() : void - { - $output = new BufferedOutput; - $output->write('one'); - $output->write('two'); - - static::assertEquals('onetwo', $output->fetch()); - } - - public function testFetchCleansBufferByDefault() : void - { - $output = new BufferedOutput; - $output->write('one'); - - static::assertEquals('one', $output->fetch()); - static::assertEquals('', $output->fetch()); - } - - public function testFetchWithoutCleaning() : void - { - $output = new BufferedOutput; - $output->write('one'); - - static::assertEquals('one', $output->fetch(false)); - - $output->write('two'); - - static::assertEquals('onetwo', $output->fetch(false)); - } -} diff --git a/test/IO/ResourceInputStreamTest.php b/test/IO/ResourceInputStreamTest.php deleted file mode 100644 index f0a3cd75..00000000 --- a/test/IO/ResourceInputStreamTest.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ -class ResourceInputStreamTest extends TestCase -{ - public function testNonStream() : void - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Expected a valid stream'); - new ResourceInputStream(42); - } - - public function testNotReadable() : void - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Expected a readable stream'); - new ResourceInputStream(\STDOUT); - } - - public function testRead() : void - { - $stream = fopen('php://memory', 'r+'); - fwrite($stream, '1234'); - rewind($stream); - - $inputStream = new ResourceInputStream($stream); - - $input = ''; - $inputStream->read(4, function ($buffer) use (&$input) { - $input .= $buffer; - }); - - static::assertSame('1234', $input); - - fclose($stream); - } -} diff --git a/test/IO/ResourceOutputStreamTest.php b/test/IO/ResourceOutputStreamTest.php deleted file mode 100644 index 17ed954a..00000000 --- a/test/IO/ResourceOutputStreamTest.php +++ /dev/null @@ -1,36 +0,0 @@ - - */ -class ResourceOutputStreamTest extends TestCase -{ - public function testNonStream() : void - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Expected a valid stream'); - new ResourceOutputStream(42); - } - - public function testNotWritable() : void - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Expected a writable stream'); - new ResourceOutputStream(\STDIN); - } - - public function testWrite() : void - { - $stream = fopen('php://memory', 'r+'); - $outputStream = new ResourceOutputStream($stream); - $outputStream->write('123456789'); - - rewind($stream); - static::assertEquals('123456789', stream_get_contents($stream)); - } -} From 19ddd2897b207c37d2734768cb3430113181ec4d Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Mon, 23 Apr 2018 19:34:20 +0200 Subject: [PATCH 08/16] Terminal has moved to php-school/terminal --- src/Terminal/TerminalFactory.php | 9 +- src/Terminal/TerminalInterface.php | 96 ----------- src/Terminal/UnixTerminal.php | 259 ----------------------------- 3 files changed, 6 insertions(+), 358 deletions(-) delete mode 100644 src/Terminal/TerminalInterface.php delete mode 100644 src/Terminal/UnixTerminal.php diff --git a/src/Terminal/TerminalFactory.php b/src/Terminal/TerminalFactory.php index 779a1c14..1c32e9f2 100644 --- a/src/Terminal/TerminalFactory.php +++ b/src/Terminal/TerminalFactory.php @@ -2,15 +2,18 @@ namespace PhpSchool\CliMenu\Terminal; -use PhpSchool\CliMenu\IO\ResourceInputStream; -use PhpSchool\CliMenu\IO\ResourceOutputStream; +use PhpSchool\Terminal\IO\ResourceInputStream; +use PhpSchool\Terminal\IO\ResourceOutputStream; +use PhpSchool\Terminal\Terminal; +use PhpSchool\Terminal\UnixTerminal; + /** * @author Michael Woodward */ class TerminalFactory { - public static function fromSystem() : TerminalInterface + public static function fromSystem() : Terminal { return new UnixTerminal(new ResourceInputStream, new ResourceOutputStream); } diff --git a/src/Terminal/TerminalInterface.php b/src/Terminal/TerminalInterface.php deleted file mode 100644 index 5a1a8787..00000000 --- a/src/Terminal/TerminalInterface.php +++ /dev/null @@ -1,96 +0,0 @@ - - */ -interface TerminalInterface -{ - /** - * Get terminal details - */ - public function getDetails() : string ; - - /** - * Get the available width of the terminal - */ - public function getWidth() : int; - - /** - * Get the available height of the terminal - */ - public function getHeight() : int; - - /** - * Toggle canonical mode on TTY - */ - public function setCanonicalMode(bool $useCanonicalMode = true) : void; - - /** - * Check if TTY is in canonical mode - */ - public function isCanonical() : bool; - - /** - * Test whether terminal is valid TTY - */ - public function isTTY() : bool; - - /** - * Test whether terminal supports colour output - */ - public function supportsColour() : bool; - - /** - * Clear the terminal window - */ - public function clear() : void; - - /** - * Clear the current cursors line - */ - public function clearLine() : void; - - /** - * Move the cursor to the top left of the window - */ - public function moveCursorToTop() : void; - - /** - * Move the cursor to the start of a specific row - */ - public function moveCursorToRow(int $rowNumber) : void; - - /** - * Move the cursor to a specific column - */ - public function moveCursorToColumn(int $columnNumber) : void; - - /** - * Clean the whole console without jumping the window - */ - public function clean() : void; - - /** - * Enable cursor display - */ - public function enableCursor() : void; - - /** - * Disable cursor display - */ - public function disableCursor() : void; - - /** - * Read input from the terminal - */ - public function getKeyedInput(array $map = []) : ?string; - - /** - * Get the output stream - */ - public function getOutput() : OutputStream; -} diff --git a/src/Terminal/UnixTerminal.php b/src/Terminal/UnixTerminal.php deleted file mode 100644 index 4fafb68c..00000000 --- a/src/Terminal/UnixTerminal.php +++ /dev/null @@ -1,259 +0,0 @@ - - */ -class UnixTerminal implements TerminalInterface -{ - /** - * @var bool - */ - private $isTTY; - - /** - * @var bool - */ - private $isCanonical = false; - - /** - * @var int - */ - private $width; - - /** - * @var int - */ - private $height; - - /** - * @var string - */ - private $details; - - /** - * @var string - */ - private $originalConfiguration; - - /** - * @var InputStream - */ - private $input; - - /** - * @var OutputStream - */ - private $output; - - /** - * Initialise the terminal from resource - * - */ - public function __construct(InputStream $input, OutputStream $output) - { - $this->getOriginalConfiguration(); - $this->input = $input; - $this->output = $output; - } - - /** - * Get the available width of the terminal - */ - public function getWidth() : int - { - return $this->width ?: $this->width = (int) exec('tput cols'); - } - - /** - * Get the available height of the terminal - */ - public function getHeight() : int - { - return $this->height ?: $this->height = (int) exec('tput lines'); - } - - /** - * Get terminal details - */ - public function getDetails() : string - { - if (!$this->details) { - $this->details = function_exists('posix_ttyname') - ? @posix_ttyname(STDOUT) - : "Can't retrieve terminal details"; - } - - return $this->details; - } - - /** - * Get the original terminal configuration / mode - */ - private function getOriginalConfiguration() : string - { - return $this->originalConfiguration ?: $this->originalConfiguration = exec('stty -g'); - } - - /** - * Toggle canonical mode on TTY - */ - public function setCanonicalMode(bool $useCanonicalMode = true) : void - { - if ($useCanonicalMode) { - exec('stty -icanon'); - $this->isCanonical = true; - } else { - exec('stty ' . $this->getOriginalConfiguration()); - $this->isCanonical = false; - } - } - - /** - * Check if TTY is in canonical mode - * Assumes the terminal was never in canonical mode - */ - public function isCanonical() : bool - { - return $this->isCanonical; - } - - /** - * Test whether terminal is valid TTY - */ - public function isTTY() : bool - { - return $this->isTTY ?: $this->isTTY = function_exists('posix_isatty') && @posix_isatty(STDOUT); - } - - /** - * Test whether terminal supports colour output - * - * @link https://github.com/symfony/Console/blob/master/Output/StreamOutput.php#L95-L102 - */ - public function supportsColour() : bool - { - if (DIRECTORY_SEPARATOR === '\\') { - return false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM'); - } - - return $this->isTTY(); - } - - /** - * @param array $map Provide an alternative map - */ - public function getKeyedInput(array $map = []) : ?string - { - // TODO: Move to class var? - // TODO: up, down, enter etc in Abstract CONSTs - - if (empty($map)) { - $map = [ - "\033[A" => 'up', - "k" => 'up', - "" => 'up', // emacs ^P - "\033[B" => 'down', - "j" => 'down', - "" => 'down', //emacs ^N - "\n" => 'enter', - "\r" => 'enter', - " " => 'enter', - "\177" => 'backspace' - ]; - } - - $input = ''; - $this->input->read(4, function ($buffer) use (&$input) { - $input .= $buffer; - }); - - $this->clearLine(); - - if ($input === false) { - return null; - } - - return array_key_exists($input, $map) - ? $map[$input] - : $input; - } - - /** - * Clear the terminal window - */ - public function clear() : void - { - $this->output->write("\033[2J"); - } - - /** - * Enable cursor - */ - public function enableCursor() : void - { - $this->output->write("\033[?25h"); - } - - /** - * Disable cursor - */ - public function disableCursor() : void - { - $this->output->write("\033[?25l"); - } - - /** - * Move the cursor to the top left of the window - * - * @return void - */ - public function moveCursorToTop() : void - { - $this->output->write("\033[H"); - } - - /** - * Move the cursor to the start of a specific row - */ - public function moveCursorToRow(int $rowNumber) : void - { - $this->output->write(sprintf("\033[%d;0H", $rowNumber)); - } - - /** - * Move the cursor to the start of a specific column - */ - public function moveCursorToColumn(int $column) : void - { - $this->output->write(sprintf("\033[%dC", $column)); - } - - /** - * Clear the current cursors line - */ - public function clearLine() : void - { - $this->output->write(sprintf("\033[%dD\033[K", $this->getWidth())); - } - - /** - * Clean the whole console without jumping the window - */ - public function clean() : void - { - foreach (range(0, $this->getHeight()) as $rowNum) { - $this->moveCursorToRow($rowNum); - $this->clearLine(); - } - } - - public function getOutput() : OutputStream - { - return $this->output; - } -} From fdcf19edab32d78409af493739a13ac5361740d4 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Mon, 23 Apr 2018 19:34:42 +0200 Subject: [PATCH 09/16] Bring in php-school/terminal --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index b5003a4d..e99c6af6 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "require": { "php" : ">=7.1", "beberlei/assert": "^2.4", + "php-school/terminal": "dev-master", "ext-posix": "*" }, "autoload" : { From 2a2e6ed60875b266bade7a883ba9217441d088c4 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Mon, 23 Apr 2018 19:37:58 +0200 Subject: [PATCH 10/16] Refactor to use php-school/terminal --- src/CliMenu.php | 85 +++++++++++------- src/CliMenuBuilder.php | 6 +- src/Dialogue/Confirm.php | 47 ++++------ src/Dialogue/Dialogue.php | 8 +- src/Dialogue/Flash.php | 8 +- src/Input/Input.php | 4 + src/Input/InputIO.php | 94 ++++++++++---------- src/Input/Number.php | 20 ++++- src/Input/Password.php | 15 +++- src/Input/Text.php | 15 +++- src/MenuStyle.php | 6 +- test/CliMenuBuilderTest.php | 4 +- test/CliMenuTest.php | 157 +++++++++++++++++++++++++++++----- test/Dialogue/ConfirmTest.php | 60 +++++++------ test/Dialogue/FlashTest.php | 43 +++++----- test/Input/InputIOTest.php | 62 ++++++++++---- test/Input/NumberTest.php | 18 ++-- test/Input/PasswordTest.php | 14 +-- test/Input/TextTest.php | 14 +-- 19 files changed, 443 insertions(+), 237 deletions(-) diff --git a/src/CliMenu.php b/src/CliMenu.php index 461b318c..b3c8b0a2 100644 --- a/src/CliMenu.php +++ b/src/CliMenu.php @@ -16,8 +16,12 @@ use PhpSchool\CliMenu\Dialogue\Confirm; use PhpSchool\CliMenu\Dialogue\Flash; use PhpSchool\CliMenu\Terminal\TerminalFactory; -use PhpSchool\CliMenu\Terminal\TerminalInterface; use PhpSchool\CliMenu\Util\StringUtil as s; +use PhpSchool\Terminal\Exception\NotInteractiveTerminal; +use PhpSchool\Terminal\InputCharacter; +use PhpSchool\Terminal\NonCanonicalReader; +use PhpSchool\Terminal\Terminal; +use PhpSchool\Terminal\TerminalReader; /** * @author Michael Woodward @@ -25,7 +29,7 @@ class CliMenu { /** - * @var TerminalInterface + * @var Terminal */ protected $terminal; @@ -67,7 +71,7 @@ class CliMenu public function __construct( ?string $title, array $items, - TerminalInterface $terminal = null, + Terminal $terminal = null, MenuStyle $style = null ) { $this->title = $title; @@ -80,40 +84,33 @@ public function __construct( /** * Configure the terminal to work with CliMenu - * - * @throws InvalidTerminalException */ protected function configureTerminal() : void { $this->assertTerminalIsValidTTY(); - $this->terminal->setCanonicalMode(); + $this->terminal->disableCanonicalMode(); + $this->terminal->disableEchoBack(); $this->terminal->disableCursor(); $this->terminal->clear(); } /** * Revert changes made to the terminal - * - * @throws InvalidTerminalException */ protected function tearDownTerminal() : void { - $this->assertTerminalIsValidTTY(); - - $this->terminal->setCanonicalMode(false); - $this->terminal->enableCursor(); + $this->terminal->restoreOriginalConfiguration(); } private function assertTerminalIsValidTTY() : void { - if (!$this->terminal->isTTY()) { - throw new InvalidTerminalException( - sprintf('Terminal "%s" is not a valid TTY', $this->terminal->getDetails()) - ); + if (!$this->terminal->isInteractive()) { + throw new InvalidTerminalException('Terminal is not interactive (TTY)'); } } + public function setParent(CliMenu $parent) : void { $this->parent = $parent; @@ -124,7 +121,7 @@ public function getParent() : ?CliMenu return $this->parent; } - public function getTerminal() : TerminalInterface + public function getTerminal() : Terminal { return $this->terminal; } @@ -166,14 +163,28 @@ private function display() : void { $this->draw(); - while ($this->isOpen() && $input = $this->terminal->getKeyedInput()) { - switch ($input) { - case 'up': - case 'down': - $this->moveSelection($input); + $reader = new NonCanonicalReader($this->terminal); + $reader->addControlMappings([ + '^P' => InputCharacter::UP, + 'k' => InputCharacter::UP, + '^K' => InputCharacter::DOWN, + 'j' => InputCharacter::DOWN, + "\r" => InputCharacter::ENTER, + ' ' => InputCharacter::ENTER, + ]); + + while ($this->isOpen() && $char = $reader->readCharacter()) { + if ($char->isNotControl()) { + continue; + } + + switch ($char->getControl()) { + case InputCharacter::UP: + case InputCharacter::DOWN: + $this->moveSelection($char->getControl()); $this->draw(); break; - case 'enter': + case InputCharacter::ENTER: $this->executeCurrentItem(); break; } @@ -188,12 +199,12 @@ protected function moveSelection(string $direction) : void do { $itemKeys = array_keys($this->items); - $direction === 'up' + $direction === 'UP' ? $this->selectedItem-- : $this->selectedItem++; if (!array_key_exists($this->selectedItem, $this->items)) { - $this->selectedItem = $direction === 'up' + $this->selectedItem = $direction === 'UP' ? end($itemKeys) : reset($itemKeys); } elseif ($this->getSelectedItem()->canSelect()) { @@ -224,12 +235,16 @@ protected function executeCurrentItem() : void * Redraw the menu */ public function redraw() : void + { + $this->assertOpen(); + $this->draw(); + } + + private function assertOpen() : void { if (!$this->isOpen()) { throw new MenuNotOpenException; } - - $this->draw(); } /** @@ -259,7 +274,7 @@ protected function draw() : void $frame->newLine(2); foreach ($frame->getRows() as $row) { - $this->terminal->getOutput()->write($row); + $this->terminal->write($row); } $this->currentFrame = $frame; @@ -282,7 +297,7 @@ protected function drawMenuItem(MenuItemInterface $item, bool $selected = false) return array_map(function ($row) use ($setColour, $unsetColour) { return sprintf( - "%s%s%s%s%s%s%s\n\r", + "%s%s%s%s%s%s%s\n", str_repeat(' ', $this->style->getMargin()), $setColour, str_repeat(' ', $this->style->getPadding()), @@ -386,29 +401,35 @@ public function confirm($text) : Confirm public function askNumber() : Number { + $this->assertOpen(); + $style = (new MenuStyle($this->terminal)) ->setBg('yellow') ->setFg('red'); - return new Number(new InputIO($this, $style, $this->terminal)); + return new Number(new InputIO($this, $this->terminal), $style); } public function askText() : Text { + $this->assertOpen(); + $style = (new MenuStyle($this->terminal)) ->setBg('yellow') ->setFg('red'); - return new Text(new InputIO($this, $style, $this->terminal)); + return new Text(new InputIO($this, $this->terminal), $style); } public function askPassword() : Password { + $this->assertOpen(); + $style = (new MenuStyle($this->terminal)) ->setBg('yellow') ->setFg('red'); - return new Password(new InputIO($this, $style, $this->terminal)); + return new Password(new InputIO($this, $this->terminal), $style); } private function guardSingleLine($text) diff --git a/src/CliMenuBuilder.php b/src/CliMenuBuilder.php index d612f2b2..6a0c09a9 100644 --- a/src/CliMenuBuilder.php +++ b/src/CliMenuBuilder.php @@ -11,8 +11,8 @@ use PhpSchool\CliMenu\MenuItem\SelectableItem; use PhpSchool\CliMenu\MenuItem\StaticItem; use PhpSchool\CliMenu\Terminal\TerminalFactory; -use PhpSchool\CliMenu\Terminal\TerminalInterface; use Assert\Assertion; +use PhpSchool\Terminal\Terminal; use RuntimeException; /** @@ -62,7 +62,7 @@ class CliMenuBuilder private $style; /** - * @var TerminalInterface + * @var Terminal */ private $terminal; @@ -262,7 +262,7 @@ public function setTitleSeparator(string $separator) : self return $this; } - public function setTerminal(TerminalInterface $terminal) : self + public function setTerminal(Terminal $terminal) : self { $this->terminal = $terminal; return $this; diff --git a/src/Dialogue/Confirm.php b/src/Dialogue/Confirm.php index be8ac4cd..68ee3f49 100644 --- a/src/Dialogue/Confirm.php +++ b/src/Dialogue/Confirm.php @@ -2,6 +2,9 @@ namespace PhpSchool\CliMenu\Dialogue; +use PhpSchool\Terminal\InputCharacter; +use PhpSchool\Terminal\NonCanonicalReader; + /** * @author Aydin Hassan */ @@ -33,35 +36,21 @@ public function display(string $confirmText = 'OK') : void $this->emptyRow(); $confirmText = sprintf(' < %s > ', $confirmText); - $leftFill = ($promptWidth / 2) - (mb_strlen($confirmText) / 2); + $leftFill = ($promptWidth / 2) - (mb_strlen($confirmText) / 2); $this->write(sprintf( - '%s%s%s', + "%s%s%s%s%s%s%s%s%s\n", $this->style->getUnselectedSetCode(), str_repeat(' ', $leftFill), - $this->style->getUnselectedSetCode() + $this->style->getUnselectedUnsetCode(), + $this->style->getSelectedSetCode(), + $confirmText, + $this->style->getSelectedUnsetCode(), + $this->style->getUnselectedSetCode(), + str_repeat(' ', ceil($promptWidth - $leftFill - mb_strlen($confirmText))), + $this->style->getUnselectedUnsetCode() )); - $this->write( - sprintf( - '%s%s%s', - $this->style->getSelectedSetCode(), - $confirmText, - $this->style->getSelectedUnsetCode() - ), - -1 - ); - - $this->write( - sprintf( - "%s%s%s\n", - $this->style->getUnselectedSetCode(), - str_repeat(' ', ceil($promptWidth - $leftFill - mb_strlen($confirmText))), - $this->style->getSelectedUnsetCode() - ), - -1 - ); - $this->write(sprintf( "%s%s%s%s%s\n", $this->style->getUnselectedSetCode(), @@ -72,12 +61,14 @@ public function display(string $confirmText = 'OK') : void )); $this->terminal->moveCursorToTop(); - $input = $this->terminal->getKeyedInput(); - while ($input !== 'enter') { - $input = $this->terminal->getKeyedInput(); - } + $reader = new NonCanonicalReader($this->terminal); - $this->parentMenu->redraw(); + while ($char = $reader->readCharacter()) { + if ($char->isControl() && $char->getControl() === InputCharacter::ENTER) { + $this->parentMenu->redraw(); + return; + } + } } } diff --git a/src/Dialogue/Dialogue.php b/src/Dialogue/Dialogue.php index 3215cb0c..f9278222 100644 --- a/src/Dialogue/Dialogue.php +++ b/src/Dialogue/Dialogue.php @@ -5,7 +5,7 @@ use PhpSchool\CliMenu\CliMenu; use PhpSchool\CliMenu\Exception\MenuNotOpenException; use PhpSchool\CliMenu\MenuStyle; -use PhpSchool\CliMenu\Terminal\TerminalInterface; +use PhpSchool\Terminal\Terminal; /** * @author Aydin Hassan @@ -23,7 +23,7 @@ abstract class Dialogue protected $parentMenu; /** - * @var TerminalInterface + * @var Terminal */ protected $terminal; @@ -42,7 +42,7 @@ abstract class Dialogue */ protected $y; - public function __construct(CliMenu $parentMenu, MenuStyle $menuStyle, TerminalInterface $terminal, string $text) + public function __construct(CliMenu $parentMenu, MenuStyle $menuStyle, Terminal $terminal, string $text) { $this->style = $menuStyle; $this->terminal = $terminal; @@ -101,7 +101,7 @@ protected function emptyRow() : void protected function write(string $text, int $column = null) : void { $this->terminal->moveCursorToColumn($column ?: $this->x); - $this->terminal->getOutput()->write($text); + $this->terminal->write($text); } public function getStyle() : MenuStyle diff --git a/src/Dialogue/Flash.php b/src/Dialogue/Flash.php index 6e9227d6..2d305da5 100644 --- a/src/Dialogue/Flash.php +++ b/src/Dialogue/Flash.php @@ -2,6 +2,8 @@ namespace PhpSchool\CliMenu\Dialogue; +use PhpSchool\Terminal\NonCanonicalReader; + /** * @author Aydin Hassan */ @@ -29,8 +31,12 @@ public function display() : void )); $this->emptyRow(); + $this->terminal->moveCursorToTop(); - $this->terminal->getKeyedInput(); + + $reader = new NonCanonicalReader($this->terminal); + $reader->readCharacter(); + $this->parentMenu->redraw(); } } diff --git a/src/Input/Input.php b/src/Input/Input.php index 089d7ec9..520eb78d 100644 --- a/src/Input/Input.php +++ b/src/Input/Input.php @@ -2,6 +2,8 @@ namespace PhpSchool\CliMenu\Input; +use PhpSchool\CliMenu\MenuStyle; + /** * @author Aydin Hassan */ @@ -24,4 +26,6 @@ public function setPlaceholderText(string $placeholderText) : Input; public function getPlaceholderText() : string; public function filter(string $value) : string; + + public function getStyle() : MenuStyle; } diff --git a/src/Input/InputIO.php b/src/Input/InputIO.php index a9811ea5..12516e55 100644 --- a/src/Input/InputIO.php +++ b/src/Input/InputIO.php @@ -3,47 +3,33 @@ namespace PhpSchool\CliMenu\Input; use PhpSchool\CliMenu\CliMenu; -use PhpSchool\CliMenu\MenuStyle; -use PhpSchool\CliMenu\Terminal\TerminalInterface; use PhpSchool\CliMenu\Util\StringUtil; +use PhpSchool\Terminal\InputCharacter; +use PhpSchool\Terminal\NonCanonicalReader; +use PhpSchool\Terminal\Terminal; /** * @author Aydin Hassan */ class InputIO { - /** - * @var MenuStyle - */ - private $style; - /** * @var CliMenu */ private $parentMenu; /** - * @var TerminalInterface + * @var Terminal */ private $terminal; - /** - * @var array - */ - private $inputMap = [ - "\n" => 'enter', - "\r" => 'enter', - "\177" => 'backspace' - ]; - /** * @var callable[][] */ private $callbacks = []; - public function __construct(CliMenu $parentMenu, MenuStyle $menuStyle, TerminalInterface $terminal) + public function __construct(CliMenu $parentMenu, Terminal $terminal) { - $this->style = $menuStyle; $this->terminal = $terminal; $this->parentMenu = $parentMenu; } @@ -53,44 +39,52 @@ public function collect(Input $input) : InputResult $this->drawInput($input, $input->getPlaceholderText()); $inputValue = $input->getPlaceholderText(); + $havePlaceHolderValue = !empty($inputValue); - while (($userInput = $this->terminal->getKeyedInput($this->inputMap)) !== null) { - $this->parentMenu->redraw(); - $this->drawInput($input, $inputValue); + $reader = new NonCanonicalReader($this->terminal); - if ($userInput === 'enter') { - if ($input->validate($inputValue)) { - $this->parentMenu->redraw(); - return new InputResult($inputValue); + while ($char = $reader->readCharacter()) { + if ($char->isNotControl()) { + if ($havePlaceHolderValue) { + $inputValue = $char->get(); + $havePlaceHolderValue = false; } else { - $this->drawInputWithError($input, $inputValue); - continue; + $inputValue .= $char->get(); } - } - if ($userInput === 'backspace') { - $inputValue = substr($inputValue, 0, -1); + $this->parentMenu->redraw(); $this->drawInput($input, $inputValue); continue; } - if (!empty($this->callbacks[$userInput])) { - foreach ($this->callbacks[$userInput] as $callback) { + switch ($char->getControl()) { + case InputCharacter::ENTER: + if ($input->validate($inputValue)) { + $this->parentMenu->redraw(); + return new InputResult($inputValue); + } else { + $this->drawInputWithError($input, $inputValue); + continue 2; + } + + case InputCharacter::BACKSPACE: + $inputValue = substr($inputValue, 0, -1); + $this->parentMenu->redraw(); + $this->drawInput($input, $inputValue); + continue 2; + } + + if (!empty($this->callbacks[$char->getControl()])) { + foreach ($this->callbacks[$char->getControl()] as $callback) { $inputValue = $callback($inputValue); $this->drawInput($input, $inputValue); } - continue; } - - $inputValue .= $userInput; - $this->drawInput($input, $inputValue); } } public function registerControlCallback(string $control, callable $callback) : void { - $this->inputMap[$control] = $control; - if (!isset($this->callbacks[$control])) { $this->callbacks[$control] = []; } @@ -135,7 +129,7 @@ private function calculateXPosition(Input $input, string $userInput) : int ); $parentStyle = $this->parentMenu->getStyle(); - $halfWidth = ($width + ($this->style->getPadding() * 2)) / 2; + $halfWidth = ($width + ($input->getStyle()->getPadding() * 2)) / 2; $parentHalfWidth = ceil($parentStyle->getWidth() / 2); return $parentHalfWidth - $halfWidth; @@ -145,14 +139,16 @@ private function drawLine(Input $input, string $userInput, string $text) : void { $this->terminal->moveCursorToColumn($this->calculateXPosition($input, $userInput)); - printf( + $line = sprintf( "%s%s%s%s%s\n", - $this->style->getUnselectedSetCode(), - str_repeat(' ', $this->style->getPadding()), + $input->getStyle()->getUnselectedSetCode(), + str_repeat(' ', $input->getStyle()->getPadding()), $text, - str_repeat(' ', $this->style->getPadding()), - $this->style->getUnselectedUnsetCode() + str_repeat(' ', $input->getStyle()->getPadding()), + $input->getStyle()->getUnselectedUnsetCode() ); + + $this->terminal->write($line); } private function drawCenteredLine(Input $input, string $userInput, string $text) : void @@ -246,11 +242,11 @@ private function drawInputField(Input $input, string $userInput) : void $userInput, sprintf( '%s%s%s%s%s', - $this->style->getUnselectedUnsetCode(), - $this->style->getSelectedSetCode(), + $input->getStyle()->getUnselectedUnsetCode(), + $input->getStyle()->getSelectedSetCode(), $userInput, - $this->style->getSelectedUnsetCode(), - $this->style->getUnselectedSetCode() + $input->getStyle()->getSelectedUnsetCode(), + $input->getStyle()->getUnselectedSetCode() ) ); } diff --git a/src/Input/Number.php b/src/Input/Number.php index a37ce9c0..358d9df9 100644 --- a/src/Input/Number.php +++ b/src/Input/Number.php @@ -2,6 +2,9 @@ namespace PhpSchool\CliMenu\Input; +use PhpSchool\CliMenu\MenuStyle; +use PhpSchool\Terminal\InputCharacter; + /** * @author Aydin Hassan */ @@ -27,9 +30,15 @@ class Number implements Input */ private $placeholderText = ''; - public function __construct(InputIO $inputIO) + /** + * @var MenuStyle + */ + private $style; + + public function __construct(InputIO $inputIO, MenuStyle $style) { $this->inputIO = $inputIO; + $this->style = $style; } public function setPromptText(string $promptText) : Input @@ -70,11 +79,11 @@ public function getPlaceholderText() : string public function ask() : InputResult { - $this->inputIO->registerControlCallback("\033[A", function (string $input) { + $this->inputIO->registerControlCallback(InputCharacter::UP, function (string $input) { return $this->validate($input) ? $input + 1 : $input; }); - $this->inputIO->registerControlCallback("\033[B", function (string $input) { + $this->inputIO->registerControlCallback(InputCharacter::DOWN, function (string $input) { return $this->validate($input) ? $input - 1 : $input; }); @@ -90,4 +99,9 @@ public function filter(string $value) : string { return $value; } + + public function getStyle() : MenuStyle + { + return $this->style; + } } diff --git a/src/Input/Password.php b/src/Input/Password.php index a52ffdd6..bc692164 100644 --- a/src/Input/Password.php +++ b/src/Input/Password.php @@ -2,6 +2,8 @@ namespace PhpSchool\CliMenu\Input; +use PhpSchool\CliMenu\MenuStyle; + /** * @author Aydin Hassan */ @@ -32,9 +34,15 @@ class Password implements Input */ private $validator; - public function __construct(InputIO $inputIO) + /** + * @var MenuStyle + */ + private $style; + + public function __construct(InputIO $inputIO, MenuStyle $style) { $this->inputIO = $inputIO; + $this->style = $style; } public function setPromptText(string $promptText) : Input @@ -97,4 +105,9 @@ public function filter(string $value) : string { return str_repeat('*', mb_strlen($value)); } + + public function getStyle() : MenuStyle + { + return $this->style; + } } diff --git a/src/Input/Text.php b/src/Input/Text.php index 58a38f90..9b99f510 100644 --- a/src/Input/Text.php +++ b/src/Input/Text.php @@ -2,6 +2,8 @@ namespace PhpSchool\CliMenu\Input; +use PhpSchool\CliMenu\MenuStyle; + /** * @author Aydin Hassan */ @@ -27,9 +29,15 @@ class Text implements Input */ private $placeholderText = ''; - public function __construct(InputIO $inputIO) + /** + * @var MenuStyle + */ + private $style; + + public function __construct(InputIO $inputIO, MenuStyle $style) { $this->inputIO = $inputIO; + $this->style = $style; } public function setPromptText(string $promptText) : Input @@ -82,4 +90,9 @@ public function filter(string $value) : string { return $value; } + + public function getStyle() : MenuStyle + { + return $this->style; + } } diff --git a/src/MenuStyle.php b/src/MenuStyle.php index c1996afe..9f630719 100644 --- a/src/MenuStyle.php +++ b/src/MenuStyle.php @@ -4,7 +4,7 @@ use PhpSchool\CliMenu\Exception\InvalidInstantiationException; use PhpSchool\CliMenu\Terminal\TerminalFactory; -use PhpSchool\CliMenu\Terminal\TerminalInterface; +use PhpSchool\Terminal\Terminal; //TODO: B/W fallback @@ -14,7 +14,7 @@ class MenuStyle { /** - * @var TerminalInterface + * @var Terminal */ protected $terminal; @@ -141,7 +141,7 @@ public static function getDefaultStyleValues() : array /** * Initialise style */ - public function __construct(TerminalInterface $terminal = null) + public function __construct(Terminal $terminal = null) { $this->terminal = $terminal ?: TerminalFactory::fromSystem(); diff --git a/test/CliMenuBuilderTest.php b/test/CliMenuBuilderTest.php index 6cd41fda..1cd73a98 100644 --- a/test/CliMenuBuilderTest.php +++ b/test/CliMenuBuilderTest.php @@ -9,7 +9,7 @@ use PhpSchool\CliMenu\MenuItem\MenuMenuItem; use PhpSchool\CliMenu\MenuItem\SelectableItem; use PhpSchool\CliMenu\MenuItem\StaticItem; -use PhpSchool\CliMenu\Terminal\TerminalInterface; +use PhpSchool\Terminal\Terminal; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -62,7 +62,7 @@ public function testModifyStyles() : void $builder->setItemExtra('*'); $builder->setTitleSeparator('-'); - $terminal = static::createMock(TerminalInterface::class); + $terminal = static::createMock(Terminal::class); $terminal ->expects($this->any()) ->method('getWidth') diff --git a/test/CliMenuTest.php b/test/CliMenuTest.php index abc4bbb0..e8a56b5e 100644 --- a/test/CliMenuTest.php +++ b/test/CliMenuTest.php @@ -4,12 +4,12 @@ use PhpSchool\CliMenu\CliMenu; use PhpSchool\CliMenu\Exception\MenuNotOpenException; -use PhpSchool\CliMenu\IO\BufferedOutput; use PhpSchool\CliMenu\MenuItem\LineBreakItem; use PhpSchool\CliMenu\MenuItem\SelectableItem; use PhpSchool\CliMenu\MenuStyle; -use PhpSchool\CliMenu\Terminal\TerminalInterface; -use PhpSchool\CliMenu\Terminal\UnixTerminal; +use PhpSchool\Terminal\Terminal; +use PhpSchool\Terminal\UnixTerminal; +use PhpSchool\Terminal\IO\BufferedOutput; use PHPUnit\Framework\TestCase; /** @@ -18,7 +18,7 @@ class CliMenuTest extends TestCase { /** - * @var TerminalInterface + * @var Terminal */ private $terminal; @@ -30,18 +30,21 @@ class CliMenuTest extends TestCase public function setUp() { $this->output = new BufferedOutput; - $this->terminal = $this->createMock(TerminalInterface::class); - $this->terminal->expects($this->any()) - ->method('getOutput') - ->willReturn($this->output); + $this->terminal = $this->createMock(Terminal::class); $this->terminal->expects($this->any()) - ->method('isTTY') + ->method('isInteractive') ->willReturn(true); $this->terminal->expects($this->any()) ->method('getWidth') ->willReturn(50); + + $this->terminal->expects($this->any()) + ->method('write') + ->will($this->returnCallback(function ($buffer){ + $this->output->write($buffer); + })); } public function testGetMenuStyle() : void @@ -66,8 +69,8 @@ public function testReDrawThrowsExceptionIfMenuNotOpen() : void public function testSimpleOpenClose() : void { $this->terminal->expects($this->once()) - ->method('getKeyedInput') - ->willReturn('enter'); + ->method('read') + ->willReturn("\n"); $style = $this->getStyle($this->terminal); @@ -78,14 +81,14 @@ public function testSimpleOpenClose() : void $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); - static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); + static::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); } public function testReDrawReDrawsImmediately() : void { $this->terminal->expects($this->once()) - ->method('getKeyedInput') - ->willReturn('enter'); + ->method('read') + ->willReturn("\n"); $style = $this->getStyle($this->terminal); @@ -98,7 +101,7 @@ public function testReDrawReDrawsImmediately() : void $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); - static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); + static::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); } public function testGetItems() : void @@ -107,7 +110,7 @@ public function testGetItems() : void $item2 = new LineBreakItem(); - $terminal = $this->createMock(TerminalInterface::class); + $terminal = $this->createMock(Terminal::class); $style = $this->getStyle($terminal); $menu = new CliMenu( @@ -128,7 +131,7 @@ public function testRemoveItem() : void $item1 = new LineBreakItem(); $item2 = new LineBreakItem(); - $terminal = $this->createMock(TerminalInterface::class); + $terminal = $this->createMock(Terminal::class); $style = $this->getStyle($terminal); $menu = new CliMenu( @@ -178,9 +181,9 @@ public function testThrowsExceptionIfTerminalIsNotValidTTY() : void { $this->expectException(\PhpSchool\CliMenu\Exception\InvalidTerminalException::class); - $terminal = $this->createMock(TerminalInterface::class); + $terminal = $this->createMock(Terminal::class); $terminal->expects($this->once()) - ->method('isTTY') + ->method('isInteractive') ->willReturn(false); $menu = new CliMenu('PHP School FTW', [], $terminal); @@ -209,12 +212,126 @@ public function testAddItem() : void $this->assertCount(1, $menu->getItems()); } + public function testAskNumberThrowsExceptionIfMenuNotOpen() : void + { + $menu = new CliMenu('PHP School FTW', []); + + static::expectException(MenuNotOpenException::class); + + $menu->askNumber(); + } + + public function testAskNumberStyle() : void + { + $terminal = $this->createMock(Terminal::class); + + $terminal->expects($this->any()) + ->method('isInteractive') + ->willReturn(true); + + $terminal->expects($this->any()) + ->method('getWidth') + ->willReturn(100); + + $terminal->expects($this->any()) + ->method('read') + ->willReturn("\n"); + + $menu = new CliMenu('PHP School FTW', [], $terminal); + + $number = null; + $menu->addItem(new SelectableItem('Ask Number', function (CliMenu $menu) use (&$number) { + $number = $menu->askNumber(); + $menu->close(); + })); + $menu->open(); + + static::assertEquals('yellow', $number->getStyle()->getBg()); + static::assertEquals('red', $number->getStyle()->getFg()); + } + + public function testAskTextThrowsExceptionIfMenuNotOpen() : void + { + $menu = new CliMenu('PHP School FTW', []); + + static::expectException(MenuNotOpenException::class); + + $menu->askText(); + } + + public function testAskTextStyle() : void + { + $terminal = $this->createMock(Terminal::class); + + $terminal->expects($this->any()) + ->method('isInteractive') + ->willReturn(true); + + $terminal->expects($this->any()) + ->method('getWidth') + ->willReturn(100); + + $terminal->expects($this->any()) + ->method('read') + ->willReturn("\n"); + + $menu = new CliMenu('PHP School FTW', [], $terminal); + + $text = null; + $menu->addItem(new SelectableItem('Ask Number', function (CliMenu $menu) use (&$text) { + $text = $menu->askText(); + $menu->close(); + })); + $menu->open(); + + static::assertEquals('yellow', $text->getStyle()->getBg()); + static::assertEquals('red', $text->getStyle()->getFg()); + } + + public function testAskPasswordThrowsExceptionIfMenuNotOpen() : void + { + $menu = new CliMenu('PHP School FTW', []); + + static::expectException(MenuNotOpenException::class); + + $menu->askPassword(); + } + + public function testAskPasswordStyle() : void + { + $terminal = $this->createMock(Terminal::class); + + $terminal->expects($this->any()) + ->method('isInteractive') + ->willReturn(true); + + $terminal->expects($this->any()) + ->method('getWidth') + ->willReturn(100); + + $terminal->expects($this->any()) + ->method('read') + ->willReturn("\n"); + + $menu = new CliMenu('PHP School FTW', [], $terminal); + + $password = null; + $menu->addItem(new SelectableItem('Ask Number', function (CliMenu $menu) use (&$password) { + $password = $menu->askPassword(); + $menu->close(); + })); + $menu->open(); + + static::assertEquals('yellow', $password->getStyle()->getBg()); + static::assertEquals('red', $password->getStyle()->getFg()); + } + private function getTestFile() : string { return sprintf('%s/res/%s.txt', __DIR__, $this->getName()); } - private function getStyle(TerminalInterface $terminal) : MenuStyle + private function getStyle(Terminal $terminal) : MenuStyle { return new MenuStyle($terminal); } diff --git a/test/Dialogue/ConfirmTest.php b/test/Dialogue/ConfirmTest.php index d97cb6a5..fbe38025 100644 --- a/test/Dialogue/ConfirmTest.php +++ b/test/Dialogue/ConfirmTest.php @@ -3,10 +3,10 @@ namespace PhpSchool\CliMenuTest\Dialogue; use PhpSchool\CliMenu\CliMenu; -use PhpSchool\CliMenu\IO\BufferedOutput; use PhpSchool\CliMenu\MenuItem\SelectableItem; use PhpSchool\CliMenu\MenuStyle; -use PhpSchool\CliMenu\Terminal\TerminalInterface; +use PhpSchool\Terminal\IO\BufferedOutput; +use PhpSchool\Terminal\Terminal; use PHPUnit\Framework\TestCase; /** @@ -27,27 +27,31 @@ class ConfirmTest extends TestCase public function setUp() { $this->output = new BufferedOutput; - $this->terminal = $this->createMock(TerminalInterface::class); - $this->terminal->expects($this->any()) - ->method('getOutput') - ->willReturn($this->output); + $this->terminal = $this->createMock(Terminal::class); $this->terminal->expects($this->any()) - ->method('isTTY') + ->method('isInteractive') ->willReturn(true); $this->terminal->expects($this->any()) ->method('getWidth') ->willReturn(50); + + $this->terminal->expects($this->any()) + ->method('write') + ->will($this->returnCallback(function ($buffer){ + $this->output->write($buffer); + })); + } public function testConfirmWithOddLengthConfirmAndButton() : void { $this->terminal - ->method('getKeyedInput') + ->method('read') ->will($this->onConsecutiveCalls( - 'enter', - 'enter' + "\n", + "\n" )); $style = $this->getStyle($this->terminal); @@ -62,16 +66,16 @@ public function testConfirmWithOddLengthConfirmAndButton() : void $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); - static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); + static::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); } public function testConfirmWithEvenLengthConfirmAndButton() : void { $this->terminal - ->method('getKeyedInput') + ->method('read') ->will($this->onConsecutiveCalls( - 'enter', - 'enter' + "\n", + "\n" )); $style = $this->getStyle($this->terminal); @@ -86,16 +90,16 @@ public function testConfirmWithEvenLengthConfirmAndButton() : void $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); - static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); + static::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); } public function testConfirmWithEvenLengthConfirmAndOddLengthButton() : void { $this->terminal - ->method('getKeyedInput') + ->method('read') ->will($this->onConsecutiveCalls( - 'enter', - 'enter' + "\n", + "\n" )); $style = $this->getStyle($this->terminal); @@ -110,16 +114,16 @@ public function testConfirmWithEvenLengthConfirmAndOddLengthButton() : void $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); - static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); + static::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); } public function testConfirmWithOddLengthConfirmAndEvenLengthButton() : void { $this->terminal - ->method('getKeyedInput') + ->method('read') ->will($this->onConsecutiveCalls( - 'enter', - 'enter' + "\n", + "\n" )); $style = $this->getStyle($this->terminal); @@ -134,18 +138,18 @@ public function testConfirmWithOddLengthConfirmAndEvenLengthButton() : void $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); - static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); + static::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); } public function testConfirmCanOnlyBeClosedWithEnter() : void { $this->terminal - ->method('getKeyedInput') + ->method('read') ->will($this->onConsecutiveCalls( - 'enter', + "\n", 'up', 'down', - 'enter' + "\n" )); $style = $this->getStyle($this->terminal); @@ -160,7 +164,7 @@ public function testConfirmCanOnlyBeClosedWithEnter() : void $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); - static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); + static::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); } private function getTestFile() : string @@ -168,7 +172,7 @@ private function getTestFile() : string return sprintf('%s/../res/%s.txt', __DIR__, $this->getName()); } - private function getStyle(TerminalInterface $terminal) : MenuStyle + private function getStyle(Terminal $terminal) : MenuStyle { return new MenuStyle($terminal); } diff --git a/test/Dialogue/FlashTest.php b/test/Dialogue/FlashTest.php index 09869ebe..18406a1d 100644 --- a/test/Dialogue/FlashTest.php +++ b/test/Dialogue/FlashTest.php @@ -3,10 +3,10 @@ namespace PhpSchool\CliMenuTest\Dialogue; use PhpSchool\CliMenu\CliMenu; -use PhpSchool\CliMenu\IO\BufferedOutput; use PhpSchool\CliMenu\MenuItem\SelectableItem; use PhpSchool\CliMenu\MenuStyle; -use PhpSchool\CliMenu\Terminal\TerminalInterface; +use PhpSchool\Terminal\IO\BufferedOutput; +use PhpSchool\Terminal\Terminal; use PHPUnit\Framework\TestCase; /** @@ -27,27 +27,30 @@ class FlashTest extends TestCase public function setUp() { $this->output = new BufferedOutput; - $this->terminal = $this->createMock(TerminalInterface::class); - $this->terminal->expects($this->any()) - ->method('getOutput') - ->willReturn($this->output); + $this->terminal = $this->createMock(Terminal::class); $this->terminal->expects($this->any()) - ->method('isTTY') + ->method('isInteractive') ->willReturn(true); $this->terminal->expects($this->any()) ->method('getWidth') ->willReturn(50); + + $this->terminal->expects($this->any()) + ->method('write') + ->will($this->returnCallback(function ($buffer){ + $this->output->write($buffer); + })); } public function testFlashWithOddLength() : void { $this->terminal - ->method('getKeyedInput') + ->method('read') ->will($this->onConsecutiveCalls( - 'enter', - 'enter' + "\n", + "\n" )); $style = $this->getStyle($this->terminal); @@ -62,16 +65,16 @@ public function testFlashWithOddLength() : void $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); - static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); + static::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); } public function testFlashWithEvenLength() : void { $this->terminal - ->method('getKeyedInput') + ->method('read') ->will($this->onConsecutiveCalls( - 'enter', - 'enter' + "\n", + "\n" )); $style = $this->getStyle($this->terminal); @@ -86,7 +89,7 @@ public function testFlashWithEvenLength() : void $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); - static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); + static::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); } /** @@ -95,8 +98,8 @@ public function testFlashWithEvenLength() : void public function testFlashCanBeClosedWithAnyKey(string $key) : void { $this->terminal - ->method('getKeyedInput') - ->will($this->onConsecutiveCalls('enter', $key)); + ->method('read') + ->will($this->onConsecutiveCalls("\n", $key)); $style = $this->getStyle($this->terminal); @@ -110,13 +113,13 @@ public function testFlashCanBeClosedWithAnyKey(string $key) : void $menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style); $menu->open(); - static::assertEquals($this->output->fetch(), file_get_contents($this->getTestFile())); + static::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); } public function keyProvider() : array { return [ - ['enter'], + ["\n"], ['right'], ['down'], ['up'], @@ -128,7 +131,7 @@ private function getTestFile() : string return sprintf('%s/../res/%s.txt', __DIR__, $this->getName(false)); } - private function getStyle(TerminalInterface $terminal) : MenuStyle + private function getStyle(Terminal $terminal) : MenuStyle { return new MenuStyle($terminal); } diff --git a/test/Input/InputIOTest.php b/test/Input/InputIOTest.php index 43f85785..58403e8a 100644 --- a/test/Input/InputIOTest.php +++ b/test/Input/InputIOTest.php @@ -6,7 +6,9 @@ use PhpSchool\CliMenu\Input\InputIO; use PhpSchool\CliMenu\Input\Text; use PhpSchool\CliMenu\MenuStyle; -use PhpSchool\CliMenu\Terminal\TerminalInterface; +use PhpSchool\Terminal\InputCharacter; +use PhpSchool\Terminal\IO\BufferedOutput; +use PhpSchool\Terminal\Terminal; use PHPUnit\Framework\TestCase; /** @@ -15,10 +17,15 @@ class InputIOTest extends TestCase { /** - * @var TerminalInterface + * @var Terminal */ private $terminal; + /** + * @var BufferedOutput + */ + private $output; + /** * @var CliMenu */ @@ -36,37 +43,54 @@ class InputIOTest extends TestCase public function setUp() { - $this->terminal = $this->createMock(TerminalInterface::class); + $this->terminal = $this->createMock(Terminal::class); + $this->output = new BufferedOutput; $this->menu = $this->createMock(CliMenu::class); - $this->style = $this->createMock(MenuStyle::class); - $this->inputIO = new InputIO($this->menu, $this->style, $this->terminal); + $this->style = new MenuStyle($this->terminal); + $this->inputIO = new InputIO($this->menu, $this->terminal); + + $this->style->setBg('yellow'); + $this->style->setFg('red'); + + $this->terminal + ->method('getWidth') + ->willReturn(100); + + $parentStyle = new MenuStyle($this->terminal); + $parentStyle->setBg('blue'); + + $this->menu + ->expects($this->any()) + ->method('getStyle') + ->willReturn($parentStyle); } public function testEnterReturnsOutputIfValid() : void { $this->terminal ->expects($this->exactly(2)) - ->method('getKeyedInput') - ->willReturn('1', 'enter'); + ->method('read') + ->willReturn('1', "\n"); - $result = $this->inputIO->collect(new Text($this->inputIO)); + $result = $this->inputIO->collect(new Text($this->inputIO, $this->style)); self::assertEquals('1', $result->fetch()); + + echo $this->output->fetch(); } public function testCustomControlFunctions() : void { - $this->inputIO->registerControlCallback('u', function ($input) { + $this->inputIO->registerControlCallback(InputCharacter::UP, function ($input) { return ++$input; }); $this->terminal ->expects($this->exactly(4)) - ->method('getKeyedInput') - ->with(["\n" => 'enter', "\r" => 'enter', "\177" => 'backspace', 'u' => 'u']) - ->willReturn('1', '0', 'u', 'enter'); + ->method('read') + ->willReturn('1', '0', "\033[A", "\n"); - $result = $this->inputIO->collect(new Text($this->inputIO)); + $result = $this->inputIO->collect(new Text($this->inputIO, $this->style)); self::assertEquals('11', $result->fetch()); } @@ -75,17 +99,17 @@ public function testBackspaceDeletesPreviousCharacter() : void { $this->terminal ->expects($this->exactly(6)) - ->method('getKeyedInput') - ->willReturn('1', '6', '7', 'backspace', 'backspace', 'enter'); + ->method('read') + ->willReturn('1', '6', '7', "\177", "\177", "\n"); - $result = $this->inputIO->collect(new Text($this->inputIO)); + $result = $this->inputIO->collect(new Text($this->inputIO, $this->style)); self::assertEquals('1', $result->fetch()); } public function testValidationErrorCausesErrorMessageToBeDisplayed() : void { - $input = new class ($this->inputIO) extends Text { + $input = new class ($this->inputIO, $this->style) extends Text { public function validate(string $input) : bool { return $input[-1] === 'p'; @@ -94,8 +118,8 @@ public function validate(string $input) : bool $this->terminal ->expects($this->exactly(6)) - ->method('getKeyedInput') - ->willReturn('1', 't', 'enter', 'backspace', 'p', 'enter'); + ->method('read') + ->willReturn('1', 't', "\n", "\177", 'p', "\n"); $result = $this->inputIO->collect($input); diff --git a/test/Input/NumberTest.php b/test/Input/NumberTest.php index 04c5a98a..e5e51669 100644 --- a/test/Input/NumberTest.php +++ b/test/Input/NumberTest.php @@ -6,7 +6,7 @@ use PhpSchool\CliMenu\Input\InputIO; use PhpSchool\CliMenu\Input\Number; use PhpSchool\CliMenu\MenuStyle; -use PhpSchool\CliMenu\Terminal\TerminalInterface; +use PhpSchool\Terminal\Terminal; use PHPUnit\Framework\TestCase; /** @@ -15,7 +15,7 @@ class NumberTest extends TestCase { /** - * @var TerminalInterface + * @var Terminal */ private $terminal; @@ -31,12 +31,12 @@ class NumberTest extends TestCase public function setUp() { - $this->terminal = $this->createMock(TerminalInterface::class); + $this->terminal = $this->createMock(Terminal::class); $menu = $this->createMock(CliMenu::class); $style = $this->createMock(MenuStyle::class); - $this->inputIO = new InputIO($menu, $style, $this->terminal); - $this->input = new Number($this->inputIO); + $this->inputIO = new InputIO($menu, $this->terminal); + $this->input = new Number($this->inputIO, $style); } public function testGetSetPromptText() : void @@ -92,8 +92,8 @@ public function testUpKeyIncrementsNumber() : void { $this->terminal ->expects($this->exactly(4)) - ->method('getKeyedInput') - ->willReturn('1', '0', "\033[A", 'enter'); + ->method('read') + ->willReturn('1', '0', "\033[A", "\n"); self::assertEquals(11, $this->input->ask()->fetch()); } @@ -102,8 +102,8 @@ public function testDownKeyDecrementsNumber() : void { $this->terminal ->expects($this->exactly(4)) - ->method('getKeyedInput') - ->willReturn('1', '0', "\033[B", 'enter'); + ->method('read') + ->willReturn('1', '0', "\033[B", "\n"); self::assertEquals(9, $this->input->ask()->fetch()); } diff --git a/test/Input/PasswordTest.php b/test/Input/PasswordTest.php index 8c9bdab3..20770061 100644 --- a/test/Input/PasswordTest.php +++ b/test/Input/PasswordTest.php @@ -6,7 +6,7 @@ use PhpSchool\CliMenu\Input\InputIO; use PhpSchool\CliMenu\Input\Password; use PhpSchool\CliMenu\MenuStyle; -use PhpSchool\CliMenu\Terminal\TerminalInterface; +use PhpSchool\Terminal\Terminal; use PHPUnit\Framework\TestCase; /** @@ -15,7 +15,7 @@ class PasswordTest extends TestCase { /** - * @var TerminalInterface + * @var Terminal */ private $terminal; @@ -31,12 +31,12 @@ class PasswordTest extends TestCase public function setUp() { - $this->terminal = $this->createMock(TerminalInterface::class); + $this->terminal = $this->createMock(Terminal::class); $menu = $this->createMock(CliMenu::class); $style = $this->createMock(MenuStyle::class); - $this->inputIO = new InputIO($menu, $style, $this->terminal); - $this->input = new Password($this->inputIO); + $this->inputIO = new InputIO($menu, $this->terminal); + $this->input = new Password($this->inputIO, $style); } public function testGetSetPromptText() : void @@ -89,8 +89,8 @@ public function testAskPassword() : void { $this->terminal ->expects($this->exactly(17)) - ->method('getKeyedInput') - ->willReturn('1', '2', '3', '4', '5', '6', '7', '8', '9', '1', '2', '3', '4', '5', '6', '7', 'enter'); + ->method('read') + ->willReturn('1', '2', '3', '4', '5', '6', '7', '8', '9', '1', '2', '3', '4', '5', '6', '7', "\n"); self::assertEquals('1234567891234567', $this->input->ask()->fetch()); } diff --git a/test/Input/TextTest.php b/test/Input/TextTest.php index 9b13470a..2b3e6125 100644 --- a/test/Input/TextTest.php +++ b/test/Input/TextTest.php @@ -6,7 +6,7 @@ use PhpSchool\CliMenu\Input\InputIO; use PhpSchool\CliMenu\Input\Text; use PhpSchool\CliMenu\MenuStyle; -use PhpSchool\CliMenu\Terminal\TerminalInterface; +use PhpSchool\Terminal\Terminal; use PHPUnit\Framework\TestCase; /** @@ -15,7 +15,7 @@ class TextTest extends TestCase { /** - * @var TerminalInterface + * @var Terminal */ private $terminal; @@ -31,12 +31,12 @@ class TextTest extends TestCase public function setUp() { - $this->terminal = $this->createMock(TerminalInterface::class); + $this->terminal = $this->createMock(Terminal::class); $menu = $this->createMock(CliMenu::class); $style = $this->createMock(MenuStyle::class); - $this->inputIO = new InputIO($menu, $style, $this->terminal); - $this->input = new Text($this->inputIO); + $this->inputIO = new InputIO($menu, $this->terminal); + $this->input = new Text($this->inputIO, $style); } public function testGetSetPromptText() : void @@ -89,8 +89,8 @@ public function testAskText() : void { $this->terminal ->expects($this->exactly(10)) - ->method('getKeyedInput') - ->willReturn('s', 'o', 'm', 'e', ' ', 't', 'e', 'x', 't', 'enter'); + ->method('read') + ->willReturn('s', 'o', 'm', 'e', ' ', 't', 'e', 'x', 't', "\n"); self::assertEquals('some text', $this->input->ask()->fetch()); } From 2b673eb2e03a5748daf5830e76144ca2aa7516a8 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Mon, 23 Apr 2018 19:39:06 +0200 Subject: [PATCH 11/16] Remove windows line endings --- .../testConfirmCanOnlyBeClosedWithEnter.txt | 22 +++++++++---------- ...tConfirmWithEvenLengthConfirmAndButton.txt | 22 +++++++++---------- ...ithEvenLengthConfirmAndOddLengthButton.txt | 22 +++++++++---------- ...stConfirmWithOddLengthConfirmAndButton.txt | 22 +++++++++---------- ...ithOddLengthConfirmAndEvenLengthButton.txt | 22 +++++++++---------- test/res/testFlashCanBeClosedWithAnyKey.txt | 20 ++++++++--------- test/res/testFlashWithEvenLength.txt | 20 ++++++++--------- test/res/testFlashWithOddLength.txt | 20 ++++++++--------- test/res/testReDrawReDrawsImmediately.txt | 20 ++++++++--------- test/res/testSimpleOpenClose.txt | 10 ++++----- 10 files changed, 100 insertions(+), 100 deletions(-) diff --git a/test/res/testConfirmCanOnlyBeClosedWithEnter.txt b/test/res/testConfirmCanOnlyBeClosedWithEnter.txt index 8d660f00..6d515273 100644 --- a/test/res/testConfirmCanOnlyBeClosedWithEnter.txt +++ b/test/res/testConfirmCanOnlyBeClosedWithEnter.txt @@ -1,23 +1,23 @@   -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   +    PHP School FTW!    -  < OK >   +  < OK >       -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   + diff --git a/test/res/testConfirmWithEvenLengthConfirmAndButton.txt b/test/res/testConfirmWithEvenLengthConfirmAndButton.txt index 614464dc..81ed512f 100644 --- a/test/res/testConfirmWithEvenLengthConfirmAndButton.txt +++ b/test/res/testConfirmWithEvenLengthConfirmAndButton.txt @@ -1,23 +1,23 @@   -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   +    PHP School FTW    -  < OK >   +  < OK >       -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   + diff --git a/test/res/testConfirmWithEvenLengthConfirmAndOddLengthButton.txt b/test/res/testConfirmWithEvenLengthConfirmAndOddLengthButton.txt index 770bd618..ab1ea82d 100644 --- a/test/res/testConfirmWithEvenLengthConfirmAndOddLengthButton.txt +++ b/test/res/testConfirmWithEvenLengthConfirmAndOddLengthButton.txt @@ -1,23 +1,23 @@   -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   +    PHP School FTW    -  < OK! >   +  < OK! >       -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   + diff --git a/test/res/testConfirmWithOddLengthConfirmAndButton.txt b/test/res/testConfirmWithOddLengthConfirmAndButton.txt index b77a3d67..9739d1df 100644 --- a/test/res/testConfirmWithOddLengthConfirmAndButton.txt +++ b/test/res/testConfirmWithOddLengthConfirmAndButton.txt @@ -1,23 +1,23 @@   -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   +    PHP School FTW!    -  < OK! >   +  < OK! >       -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   + diff --git a/test/res/testConfirmWithOddLengthConfirmAndEvenLengthButton.txt b/test/res/testConfirmWithOddLengthConfirmAndEvenLengthButton.txt index 8d660f00..6d515273 100644 --- a/test/res/testConfirmWithOddLengthConfirmAndEvenLengthButton.txt +++ b/test/res/testConfirmWithOddLengthConfirmAndEvenLengthButton.txt @@ -1,23 +1,23 @@   -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   +    PHP School FTW!    -  < OK >   +  < OK >       -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   + diff --git a/test/res/testFlashCanBeClosedWithAnyKey.txt b/test/res/testFlashCanBeClosedWithAnyKey.txt index c06ba9ae..ce963387 100644 --- a/test/res/testFlashCanBeClosedWithAnyKey.txt +++ b/test/res/testFlashCanBeClosedWithAnyKey.txt @@ -1,11 +1,11 @@   -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   +    PHP School FTW!  @@ -13,9 +13,9 @@   -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   + diff --git a/test/res/testFlashWithEvenLength.txt b/test/res/testFlashWithEvenLength.txt index 6fba08a7..a32aedd0 100644 --- a/test/res/testFlashWithEvenLength.txt +++ b/test/res/testFlashWithEvenLength.txt @@ -1,11 +1,11 @@   -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   +    PHP School FTW  @@ -13,9 +13,9 @@   -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   + diff --git a/test/res/testFlashWithOddLength.txt b/test/res/testFlashWithOddLength.txt index c06ba9ae..ce963387 100644 --- a/test/res/testFlashWithOddLength.txt +++ b/test/res/testFlashWithOddLength.txt @@ -1,11 +1,11 @@   -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   +    PHP School FTW!  @@ -13,9 +13,9 @@   -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   + diff --git a/test/res/testReDrawReDrawsImmediately.txt b/test/res/testReDrawReDrawsImmediately.txt index 4ace7f16..0867fb87 100644 --- a/test/res/testReDrawReDrawsImmediately.txt +++ b/test/res/testReDrawReDrawsImmediately.txt @@ -1,18 +1,18 @@   -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   +   -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   + diff --git a/test/res/testSimpleOpenClose.txt b/test/res/testSimpleOpenClose.txt index dd97f8c2..dfd69aef 100644 --- a/test/res/testSimpleOpenClose.txt +++ b/test/res/testSimpleOpenClose.txt @@ -1,9 +1,9 @@   -  PHP School FTW  -  ==========================================  -  ● Item 1  -   - +  PHP School FTW  +  ==========================================  +  ● Item 1  +   + From 03c371a318ff26d218592a3f52a8c2a052ec0026 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Mon, 23 Apr 2018 19:40:48 +0200 Subject: [PATCH 12/16] CS --- src/Terminal/TerminalFactory.php | 1 - test/CliMenuTest.php | 2 +- test/Dialogue/ConfirmTest.php | 3 +-- test/Dialogue/FlashTest.php | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Terminal/TerminalFactory.php b/src/Terminal/TerminalFactory.php index 1c32e9f2..6a5e589c 100644 --- a/src/Terminal/TerminalFactory.php +++ b/src/Terminal/TerminalFactory.php @@ -7,7 +7,6 @@ use PhpSchool\Terminal\Terminal; use PhpSchool\Terminal\UnixTerminal; - /** * @author Michael Woodward */ diff --git a/test/CliMenuTest.php b/test/CliMenuTest.php index e8a56b5e..49419ca5 100644 --- a/test/CliMenuTest.php +++ b/test/CliMenuTest.php @@ -42,7 +42,7 @@ public function setUp() $this->terminal->expects($this->any()) ->method('write') - ->will($this->returnCallback(function ($buffer){ + ->will($this->returnCallback(function ($buffer) { $this->output->write($buffer); })); } diff --git a/test/Dialogue/ConfirmTest.php b/test/Dialogue/ConfirmTest.php index fbe38025..18ce0fd5 100644 --- a/test/Dialogue/ConfirmTest.php +++ b/test/Dialogue/ConfirmTest.php @@ -39,10 +39,9 @@ public function setUp() $this->terminal->expects($this->any()) ->method('write') - ->will($this->returnCallback(function ($buffer){ + ->will($this->returnCallback(function ($buffer) { $this->output->write($buffer); })); - } public function testConfirmWithOddLengthConfirmAndButton() : void diff --git a/test/Dialogue/FlashTest.php b/test/Dialogue/FlashTest.php index 18406a1d..51b8c852 100644 --- a/test/Dialogue/FlashTest.php +++ b/test/Dialogue/FlashTest.php @@ -39,7 +39,7 @@ public function setUp() $this->terminal->expects($this->any()) ->method('write') - ->will($this->returnCallback(function ($buffer){ + ->will($this->returnCallback(function ($buffer) { $this->output->write($buffer); })); } From 422d5e9382a601bdadc46eb96a580326ae98bfa3 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Sun, 29 Apr 2018 13:14:52 +0200 Subject: [PATCH 13/16] Allow decrement to work with negative numbers --- src/Input/Number.php | 2 +- test/Input/NumberTest.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Input/Number.php b/src/Input/Number.php index 358d9df9..f2db5edf 100644 --- a/src/Input/Number.php +++ b/src/Input/Number.php @@ -92,7 +92,7 @@ public function ask() : InputResult public function validate(string $input) : bool { - return (bool) preg_match('/^\d+$/', $input); + return (bool) preg_match('/^-?\d+$/', $input); } public function filter(string $value) : string diff --git a/test/Input/NumberTest.php b/test/Input/NumberTest.php index e5e51669..6fa2f94a 100644 --- a/test/Input/NumberTest.php +++ b/test/Input/NumberTest.php @@ -80,6 +80,11 @@ public function validateProvider() : array ['0', true], ['0000000000', true], ['9999999999', true], + ['-9999999999', true], + ['-54', true], + ['-1', true], + ['-t10', false], + ['-t', false], ]; } From 70bf6e51da9c6fdfbc8dd6081d52c934b882fe88 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Sun, 29 Apr 2018 14:56:01 +0200 Subject: [PATCH 14/16] Allow to set validation failed message in the custom validator --- src/Input/InputIO.php | 5 +---- src/Input/Password.php | 7 ++++++- test/Input/PasswordTest.php | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Input/InputIO.php b/src/Input/InputIO.php index 12516e55..d8e004cc 100644 --- a/src/Input/InputIO.php +++ b/src/Input/InputIO.php @@ -217,10 +217,7 @@ private function drawInputWithError(Input $input, string $userInput) : void $this->drawCenteredLine( $input, $userInput, - sprintf( - '%s', - $input->getValidationFailedText() - ) + $input->getValidationFailedText() ); $this->drawEmptyLine($input, $userInput); } diff --git a/src/Input/Password.php b/src/Input/Password.php index bc692164..d5e3d638 100644 --- a/src/Input/Password.php +++ b/src/Input/Password.php @@ -81,7 +81,7 @@ public function getPlaceholderText() : string return $this->placeholderText; } - public function setValidator(callable $validator) + public function setValidator(callable $validator) : void { $this->validator = $validator; } @@ -95,6 +95,11 @@ public function validate(string $input) : bool { if ($this->validator) { $validator = $this->validator; + + if ($validator instanceof \Closure) { + $validator = $validator->bindTo($this); + } + return $validator($input); } diff --git a/test/Input/PasswordTest.php b/test/Input/PasswordTest.php index 20770061..c38b8401 100644 --- a/test/Input/PasswordTest.php +++ b/test/Input/PasswordTest.php @@ -119,4 +119,21 @@ public function customValidateProvider() : array ['999ppp', true], ]; } + + public function testWithCustomValidatorAndCustomValidationMessage() : void + { + $customValidate = function ($input) { + if ($input === 'mypassword') { + $this->setValidationFailedText('Password too generic'); + return false; + } + return true; + }; + + $this->input->setValidator($customValidate); + + self::assertTrue($this->input->validate('superstrongpassword')); + self::assertFalse($this->input->validate('mypassword')); + self::assertEquals('Password too generic', $this->input->getValidationFailedText()); + } } From 27779f89c9c28181158b20fe3d5b91f7a0b79697 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Sun, 29 Apr 2018 15:10:11 +0200 Subject: [PATCH 15/16] Update password ask example to illustrate custom validation message --- examples/input-password.php | 11 +++++++++++ src/Input/Password.php | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/examples/input-password.php b/examples/input-password.php index c7d4406f..86d1a2c9 100644 --- a/examples/input-password.php +++ b/examples/input-password.php @@ -8,6 +8,17 @@ $itemCallable = function (CliMenu $menu) { $result = $menu->askPassword() ->setPlaceholderText('') + ->setValidator(function ($password) { + if ($password === 'password') { + $this->setValidationFailedText('Password is too weak'); + return false; + } else if (strlen($password) <= 6) { + $this->setValidationFailedText('Password is not long enough'); + return false; + } + + return true; + }) ->ask(); var_dump($result->fetch()); diff --git a/src/Input/Password.php b/src/Input/Password.php index d5e3d638..c4daa7c6 100644 --- a/src/Input/Password.php +++ b/src/Input/Password.php @@ -81,9 +81,11 @@ public function getPlaceholderText() : string return $this->placeholderText; } - public function setValidator(callable $validator) : void + public function setValidator(callable $validator) : Input { $this->validator = $validator; + + return $this; } public function ask() : InputResult From 32b500ad381cfde557cdccda0cc78ebf33e27ef8 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Sun, 29 Apr 2018 16:30:00 +0200 Subject: [PATCH 16/16] Only switch handled controls --- composer.json | 2 +- src/CliMenu.php | 2 +- src/Input/InputIO.php | 38 ++++++++++++++++++++------------------ 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/composer.json b/composer.json index e99c6af6..0af52afb 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "require": { "php" : ">=7.1", "beberlei/assert": "^2.4", - "php-school/terminal": "dev-master", + "php-school/terminal": "dev-catch-all-controls", "ext-posix": "*" }, "autoload" : { diff --git a/src/CliMenu.php b/src/CliMenu.php index b3c8b0a2..99d2bcc3 100644 --- a/src/CliMenu.php +++ b/src/CliMenu.php @@ -174,7 +174,7 @@ private function display() : void ]); while ($this->isOpen() && $char = $reader->readCharacter()) { - if ($char->isNotControl()) { + if (!$char->isHandledControl()) { continue; } diff --git a/src/Input/InputIO.php b/src/Input/InputIO.php index d8e004cc..ae655cfd 100644 --- a/src/Input/InputIO.php +++ b/src/Input/InputIO.php @@ -57,27 +57,29 @@ public function collect(Input $input) : InputResult continue; } - switch ($char->getControl()) { - case InputCharacter::ENTER: - if ($input->validate($inputValue)) { + if ($char->isHandledControl()) { + switch ($char->getControl()) { + case InputCharacter::ENTER: + if ($input->validate($inputValue)) { + $this->parentMenu->redraw(); + return new InputResult($inputValue); + } else { + $this->drawInputWithError($input, $inputValue); + continue 2; + } + + case InputCharacter::BACKSPACE: + $inputValue = substr($inputValue, 0, -1); $this->parentMenu->redraw(); - return new InputResult($inputValue); - } else { - $this->drawInputWithError($input, $inputValue); + $this->drawInput($input, $inputValue); continue 2; - } - - case InputCharacter::BACKSPACE: - $inputValue = substr($inputValue, 0, -1); - $this->parentMenu->redraw(); - $this->drawInput($input, $inputValue); - continue 2; - } + } - if (!empty($this->callbacks[$char->getControl()])) { - foreach ($this->callbacks[$char->getControl()] as $callback) { - $inputValue = $callback($inputValue); - $this->drawInput($input, $inputValue); + if (!empty($this->callbacks[$char->getControl()])) { + foreach ($this->callbacks[$char->getControl()] as $callback) { + $inputValue = $callback($inputValue); + $this->drawInput($input, $inputValue); + } } } }