Skip to content

Commit 4c883fb

Browse files
authored
Merge pull request #81 from php-school/inputs-new
Inputs
2 parents 6270594 + 32b500a commit 4c883fb

39 files changed

+1772
-676
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"require": {
2222
"php" : ">=7.1",
2323
"beberlei/assert": "^2.4",
24+
"php-school/terminal": "dev-catch-all-controls",
2425
"ext-posix": "*"
2526
},
2627
"autoload" : {

examples/input-advanced.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
use PhpSchool\CliMenu\CliMenu;
4+
use PhpSchool\CliMenu\CliMenuBuilder;
5+
6+
require_once(__DIR__ . '/../vendor/autoload.php');
7+
8+
$itemCallable = function (CliMenu $menu) {
9+
$username = $menu->askText()
10+
->setPromptText('Enter username')
11+
->setPlaceholderText('alice')
12+
->ask();
13+
14+
$age = $menu->askNumber()
15+
->setPromptText('Enter age')
16+
->setPlaceholderText('28')
17+
->ask();
18+
19+
$password = $menu->askPassword()
20+
->setPromptText('Enter password')
21+
->ask();
22+
23+
var_dump($username->fetch(), $age->fetch(), $password->fetch());
24+
};
25+
26+
$menu = (new CliMenuBuilder)
27+
->setTitle('User Manager')
28+
->addItem('Create New User', $itemCallable)
29+
->addLineBreak('-')
30+
->build();
31+
32+
$menu->open();

examples/input-number.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
use PhpSchool\CliMenu\CliMenu;
4+
use PhpSchool\CliMenu\CliMenuBuilder;
5+
6+
require_once(__DIR__ . '/../vendor/autoload.php');
7+
8+
$itemCallable = function (CliMenu $menu) {
9+
$result = $menu->askNumber()
10+
->setPlaceholderText(10)
11+
->ask();
12+
13+
var_dump($result->fetch());
14+
};
15+
16+
$menu = (new CliMenuBuilder)
17+
->setTitle('Basic CLI Menu')
18+
->addItem('Enter number', $itemCallable)
19+
->addItem('Second Item', $itemCallable)
20+
->addItem('Third Item', $itemCallable)
21+
->addLineBreak('-')
22+
->build();
23+
24+
$menu->open();

examples/input-password.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
use PhpSchool\CliMenu\CliMenu;
4+
use PhpSchool\CliMenu\CliMenuBuilder;
5+
6+
require_once(__DIR__ . '/../vendor/autoload.php');
7+
8+
$itemCallable = function (CliMenu $menu) {
9+
$result = $menu->askPassword()
10+
->setPlaceholderText('')
11+
->setValidator(function ($password) {
12+
if ($password === 'password') {
13+
$this->setValidationFailedText('Password is too weak');
14+
return false;
15+
} else if (strlen($password) <= 6) {
16+
$this->setValidationFailedText('Password is not long enough');
17+
return false;
18+
}
19+
20+
return true;
21+
})
22+
->ask();
23+
24+
var_dump($result->fetch());
25+
};
26+
27+
$menu = (new CliMenuBuilder)
28+
->setTitle('Basic CLI Menu')
29+
->addItem('Enter password', $itemCallable)
30+
->addItem('Second Item', $itemCallable)
31+
->addItem('Third Item', $itemCallable)
32+
->addLineBreak('-')
33+
->build();
34+
35+
$menu->open();

examples/input-text.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
use PhpSchool\CliMenu\CliMenu;
4+
use PhpSchool\CliMenu\CliMenuBuilder;
5+
6+
require_once(__DIR__ . '/../vendor/autoload.php');
7+
8+
$itemCallable = function (CliMenu $menu) {
9+
$result = $menu->askText()
10+
->setPlaceholderText('Enter something here')
11+
->ask();
12+
13+
var_dump($result->fetch());
14+
};
15+
16+
$menu = (new CliMenuBuilder)
17+
->setTitle('Basic CLI Menu')
18+
->addItem('Enter text', $itemCallable)
19+
->addItem('Second Item', $itemCallable)
20+
->addItem('Third Item', $itemCallable)
21+
->addLineBreak('-')
22+
->build();
23+
24+
$menu->open();

src/CliMenu.php

Lines changed: 91 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,34 @@
22

33
namespace PhpSchool\CliMenu;
44

5+
use PhpSchool\CliMenu\Dialogue\NumberInput;
56
use PhpSchool\CliMenu\Exception\InvalidInstantiationException;
67
use PhpSchool\CliMenu\Exception\InvalidTerminalException;
78
use PhpSchool\CliMenu\Exception\MenuNotOpenException;
9+
use PhpSchool\CliMenu\Input\InputIO;
10+
use PhpSchool\CliMenu\Input\Number;
11+
use PhpSchool\CliMenu\Input\Password;
12+
use PhpSchool\CliMenu\Input\Text;
813
use PhpSchool\CliMenu\MenuItem\LineBreakItem;
914
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
1015
use PhpSchool\CliMenu\MenuItem\StaticItem;
1116
use PhpSchool\CliMenu\Dialogue\Confirm;
1217
use PhpSchool\CliMenu\Dialogue\Flash;
1318
use PhpSchool\CliMenu\Terminal\TerminalFactory;
14-
use PhpSchool\CliMenu\Terminal\TerminalInterface;
1519
use PhpSchool\CliMenu\Util\StringUtil as s;
20+
use PhpSchool\Terminal\Exception\NotInteractiveTerminal;
21+
use PhpSchool\Terminal\InputCharacter;
22+
use PhpSchool\Terminal\NonCanonicalReader;
23+
use PhpSchool\Terminal\Terminal;
24+
use PhpSchool\Terminal\TerminalReader;
1625

1726
/**
1827
* @author Michael Woodward <[email protected]>
1928
*/
2029
class CliMenu
2130
{
2231
/**
23-
* @var TerminalInterface
32+
* @var Terminal
2433
*/
2534
protected $terminal;
2635

@@ -62,7 +71,7 @@ class CliMenu
6271
public function __construct(
6372
?string $title,
6473
array $items,
65-
TerminalInterface $terminal = null,
74+
Terminal $terminal = null,
6675
MenuStyle $style = null
6776
) {
6877
$this->title = $title;
@@ -75,40 +84,33 @@ public function __construct(
7584

7685
/**
7786
* Configure the terminal to work with CliMenu
78-
*
79-
* @throws InvalidTerminalException
8087
*/
8188
protected function configureTerminal() : void
8289
{
8390
$this->assertTerminalIsValidTTY();
8491

85-
$this->terminal->setCanonicalMode();
92+
$this->terminal->disableCanonicalMode();
93+
$this->terminal->disableEchoBack();
8694
$this->terminal->disableCursor();
8795
$this->terminal->clear();
8896
}
8997

9098
/**
9199
* Revert changes made to the terminal
92-
*
93-
* @throws InvalidTerminalException
94100
*/
95101
protected function tearDownTerminal() : void
96102
{
97-
$this->assertTerminalIsValidTTY();
98-
99-
$this->terminal->setCanonicalMode(false);
100-
$this->terminal->enableCursor();
103+
$this->terminal->restoreOriginalConfiguration();
101104
}
102105

103106
private function assertTerminalIsValidTTY() : void
104107
{
105-
if (!$this->terminal->isTTY()) {
106-
throw new InvalidTerminalException(
107-
sprintf('Terminal "%s" is not a valid TTY', $this->terminal->getDetails())
108-
);
108+
if (!$this->terminal->isInteractive()) {
109+
throw new InvalidTerminalException('Terminal is not interactive (TTY)');
109110
}
110111
}
111112

113+
112114
public function setParent(CliMenu $parent) : void
113115
{
114116
$this->parent = $parent;
@@ -119,7 +121,7 @@ public function getParent() : ?CliMenu
119121
return $this->parent;
120122
}
121123

122-
public function getTerminal() : TerminalInterface
124+
public function getTerminal() : Terminal
123125
{
124126
return $this->terminal;
125127
}
@@ -161,14 +163,28 @@ private function display() : void
161163
{
162164
$this->draw();
163165

164-
while ($this->isOpen() && $input = $this->terminal->getKeyedInput()) {
165-
switch ($input) {
166-
case 'up':
167-
case 'down':
168-
$this->moveSelection($input);
166+
$reader = new NonCanonicalReader($this->terminal);
167+
$reader->addControlMappings([
168+
'^P' => InputCharacter::UP,
169+
'k' => InputCharacter::UP,
170+
'^K' => InputCharacter::DOWN,
171+
'j' => InputCharacter::DOWN,
172+
"\r" => InputCharacter::ENTER,
173+
' ' => InputCharacter::ENTER,
174+
]);
175+
176+
while ($this->isOpen() && $char = $reader->readCharacter()) {
177+
if (!$char->isHandledControl()) {
178+
continue;
179+
}
180+
181+
switch ($char->getControl()) {
182+
case InputCharacter::UP:
183+
case InputCharacter::DOWN:
184+
$this->moveSelection($char->getControl());
169185
$this->draw();
170186
break;
171-
case 'enter':
187+
case InputCharacter::ENTER:
172188
$this->executeCurrentItem();
173189
break;
174190
}
@@ -183,12 +199,12 @@ protected function moveSelection(string $direction) : void
183199
do {
184200
$itemKeys = array_keys($this->items);
185201

186-
$direction === 'up'
202+
$direction === 'UP'
187203
? $this->selectedItem--
188204
: $this->selectedItem++;
189205

190206
if (!array_key_exists($this->selectedItem, $this->items)) {
191-
$this->selectedItem = $direction === 'up'
207+
$this->selectedItem = $direction === 'UP'
192208
? end($itemKeys)
193209
: reset($itemKeys);
194210
} elseif ($this->getSelectedItem()->canSelect()) {
@@ -219,12 +235,16 @@ protected function executeCurrentItem() : void
219235
* Redraw the menu
220236
*/
221237
public function redraw() : void
238+
{
239+
$this->assertOpen();
240+
$this->draw();
241+
}
242+
243+
private function assertOpen() : void
222244
{
223245
if (!$this->isOpen()) {
224246
throw new MenuNotOpenException;
225247
}
226-
227-
$this->draw();
228248
}
229249

230250
/**
@@ -254,7 +274,7 @@ protected function draw() : void
254274
$frame->newLine(2);
255275

256276
foreach ($frame->getRows() as $row) {
257-
echo $row;
277+
$this->terminal->write($row);
258278
}
259279

260280
$this->currentFrame = $frame;
@@ -277,7 +297,7 @@ protected function drawMenuItem(MenuItemInterface $item, bool $selected = false)
277297

278298
return array_map(function ($row) use ($setColour, $unsetColour) {
279299
return sprintf(
280-
"%s%s%s%s%s%s%s\n\r",
300+
"%s%s%s%s%s%s%s\n",
281301
str_repeat(' ', $this->style->getMargin()),
282302
$setColour,
283303
str_repeat(' ', $this->style->getPadding()),
@@ -359,9 +379,7 @@ public function getCurrentFrame() : Frame
359379

360380
public function flash(string $text) : Flash
361381
{
362-
if (strpos($text, "\n") !== false) {
363-
throw new \InvalidArgumentException;
364-
}
382+
$this->guardSingleLine($text);
365383

366384
$style = (new MenuStyle($this->terminal))
367385
->setBg('yellow')
@@ -372,14 +390,52 @@ public function flash(string $text) : Flash
372390

373391
public function confirm($text) : Confirm
374392
{
375-
if (strpos($text, "\n") !== false) {
376-
throw new \InvalidArgumentException;
377-
}
393+
$this->guardSingleLine($text);
378394

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

383399
return new Confirm($this, $style, $this->terminal, $text);
384400
}
401+
402+
public function askNumber() : Number
403+
{
404+
$this->assertOpen();
405+
406+
$style = (new MenuStyle($this->terminal))
407+
->setBg('yellow')
408+
->setFg('red');
409+
410+
return new Number(new InputIO($this, $this->terminal), $style);
411+
}
412+
413+
public function askText() : Text
414+
{
415+
$this->assertOpen();
416+
417+
$style = (new MenuStyle($this->terminal))
418+
->setBg('yellow')
419+
->setFg('red');
420+
421+
return new Text(new InputIO($this, $this->terminal), $style);
422+
}
423+
424+
public function askPassword() : Password
425+
{
426+
$this->assertOpen();
427+
428+
$style = (new MenuStyle($this->terminal))
429+
->setBg('yellow')
430+
->setFg('red');
431+
432+
return new Password(new InputIO($this, $this->terminal), $style);
433+
}
434+
435+
private function guardSingleLine($text)
436+
{
437+
if (strpos($text, "\n") !== false) {
438+
throw new \InvalidArgumentException;
439+
}
440+
}
385441
}

0 commit comments

Comments
 (0)