Skip to content

Inputs #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Apr 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"require": {
"php" : ">=7.1",
"beberlei/assert": "^2.4",
"php-school/terminal": "dev-catch-all-controls",
"ext-posix": "*"
},
"autoload" : {
Expand Down
32 changes: 32 additions & 0 deletions examples/input-advanced.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

use PhpSchool\CliMenu\CliMenu;
use PhpSchool\CliMenu\CliMenuBuilder;

require_once(__DIR__ . '/../vendor/autoload.php');

$itemCallable = function (CliMenu $menu) {
$username = $menu->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();
24 changes: 24 additions & 0 deletions examples/input-number.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

use PhpSchool\CliMenu\CliMenu;
use PhpSchool\CliMenu\CliMenuBuilder;

require_once(__DIR__ . '/../vendor/autoload.php');

$itemCallable = function (CliMenu $menu) {
$result = $menu->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();
35 changes: 35 additions & 0 deletions examples/input-password.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

use PhpSchool\CliMenu\CliMenu;
use PhpSchool\CliMenu\CliMenuBuilder;

require_once(__DIR__ . '/../vendor/autoload.php');

$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());
};

$menu = (new CliMenuBuilder)
->setTitle('Basic CLI Menu')
->addItem('Enter password', $itemCallable)
->addItem('Second Item', $itemCallable)
->addItem('Third Item', $itemCallable)
->addLineBreak('-')
->build();

$menu->open();
24 changes: 24 additions & 0 deletions examples/input-text.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

use PhpSchool\CliMenu\CliMenu;
use PhpSchool\CliMenu\CliMenuBuilder;

require_once(__DIR__ . '/../vendor/autoload.php');

$itemCallable = function (CliMenu $menu) {
$result = $menu->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();
126 changes: 91 additions & 35 deletions src/CliMenu.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,34 @@

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;
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 <mikeymike.mw@gmail.com>
*/
class CliMenu
{
/**
* @var TerminalInterface
* @var Terminal
*/
protected $terminal;

Expand Down Expand Up @@ -62,7 +71,7 @@ class CliMenu
public function __construct(
?string $title,
array $items,
TerminalInterface $terminal = null,
Terminal $terminal = null,
MenuStyle $style = null
) {
$this->title = $title;
Expand All @@ -75,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)');
Copy link
Member Author

Choose a reason for hiding this comment

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

This is what I mentioned in the php-school/terminal PR. If it's not a valid TTY - then there is no name.

}
}


public function setParent(CliMenu $parent) : void
{
$this->parent = $parent;
Expand All @@ -119,7 +121,7 @@ public function getParent() : ?CliMenu
return $this->parent;
}

public function getTerminal() : TerminalInterface
public function getTerminal() : Terminal
{
return $this->terminal;
}
Expand Down Expand Up @@ -161,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->isHandledControl()) {
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;
}
Expand All @@ -183,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()) {
Expand Down Expand Up @@ -219,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();
}

/**
Expand Down Expand Up @@ -254,7 +274,7 @@ protected function draw() : void
$frame->newLine(2);

foreach ($frame->getRows() as $row) {
echo $row;
$this->terminal->write($row);
}

$this->currentFrame = $frame;
Expand All @@ -277,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",
Copy link
Member Author

Choose a reason for hiding this comment

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

dropped this \r because we don't support windows anymore and phpstorm keep changing the line endings when I was editing the test files which was mega annoying.

"%s%s%s%s%s%s%s\n",
str_repeat(' ', $this->style->getMargin()),
$setColour,
str_repeat(' ', $this->style->getPadding()),
Expand Down Expand Up @@ -359,9 +379,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')
Expand All @@ -372,14 +390,52 @@ 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')
->setFg('red');

return new Confirm($this, $style, $this->terminal, $text);
}

public function askNumber() : Number
{
$this->assertOpen();

$style = (new MenuStyle($this->terminal))
->setBg('yellow')
->setFg('red');

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, $this->terminal), $style);
}

public function askPassword() : Password
{
$this->assertOpen();

$style = (new MenuStyle($this->terminal))
->setBg('yellow')
->setFg('red');

return new Password(new InputIO($this, $this->terminal), $style);
}

private function guardSingleLine($text)
{
if (strpos($text, "\n") !== false) {
throw new \InvalidArgumentException;
}
}
}
Loading