Skip to content
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
29 changes: 29 additions & 0 deletions examples/confirm-cancellable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

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

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

$itemCallable = function (CliMenu $menu) {
$continue = $menu->cancellableConfirm('PHP School FTW!', null, true)
->display('OK', 'Cancel');

if ($continue) {
// Something Destructive
echo "OK";
} else {
// Do nothing
echo "Cancel";
}
};

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

$menu->open();
12 changes: 12 additions & 0 deletions src/CliMenu.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PhpSchool\CliMenu;

use PhpSchool\CliMenu\Dialogue\CancellableConfirm;
use PhpSchool\CliMenu\Exception\InvalidTerminalException;
use PhpSchool\CliMenu\Exception\MenuNotOpenException;
use PhpSchool\CliMenu\Input\InputIO;
Expand Down Expand Up @@ -708,6 +709,17 @@ public function confirm(string $text, MenuStyle $style = null) : Confirm
return new Confirm($this, $style, $this->terminal, $text);
}

public function cancellableConfirm(string $text, MenuStyle $style = null) : CancellableConfirm
{
$this->guardSingleLine($text);

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

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

public function askNumber(MenuStyle $style = null) : Number
{
$this->assertOpen();
Expand Down
111 changes: 111 additions & 0 deletions src/Dialogue/CancellableConfirm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

namespace PhpSchool\CliMenu\Dialogue;

use PhpSchool\Terminal\InputCharacter;
use PhpSchool\Terminal\NonCanonicalReader;

/**
* @author Aydin Hassan <[email protected]>
*/
class CancellableConfirm extends Dialogue
{
/**
* @var bool
*/
private $confirm = true;

/**
* Display confirmation with a button with the given text
*/
public function display(string $confirmText = 'OK', string $cancelText = 'Cancel') : bool
{
$this->drawDialog($confirmText, $cancelText);

$reader = new NonCanonicalReader($this->terminal);

while ($char = $reader->readCharacter()) {
if ($char->isControl() && $char->getControl() === InputCharacter::ENTER) {
$this->parentMenu->redraw();
return $this->confirm;
} elseif ($char->isControl() && $char->getControl() === InputCharacter::TAB ||
($char->isControl() && $char->getControl() === InputCharacter::RIGHT && $this->confirm) ||
($char->isControl() && $char->getControl() === InputCharacter::LEFT && !$this->confirm)
) {
$this->confirm = !$this->confirm;
$this->drawDialog($confirmText, $cancelText);
}
}
}

private function drawDialog(string $confirmText = 'OK', string $cancelText = 'Cancel'): void
{
$this->assertMenuOpen();

$this->terminal->moveCursorToRow($this->y);

$promptWidth = mb_strlen($this->text) + 4;

$buttonLength = mb_strlen($confirmText) + 6;
$buttonLength += mb_strlen($cancelText) + 7;

$confirmButton = sprintf(
'%s%s < %s > %s%s',
$this->style->getOptionCode($this->confirm ? 'bold' : 'dim'),
$this->style->getInvertedColoursSetCode(),
$confirmText,
$this->style->getInvertedColoursUnsetCode(),
$this->style->getOptionCode($this->confirm ? 'bold' : 'dim', false)
);

$cancelButton = sprintf(
'%s%s < %s > %s%s',
$this->style->getOptionCode($this->confirm ? 'dim' : 'bold'),
$this->style->getInvertedColoursSetCode(),
$cancelText,
$this->style->getInvertedColoursUnsetCode(),
$this->style->getOptionCode($this->confirm ? 'dim' : 'bold', false)
);

$buttonRow = $confirmButton . " " . $cancelButton;

if ($promptWidth < $buttonLength) {
$pad = ($buttonLength - $promptWidth) / 2;
$this->text = sprintf(
'%s%s%s',
str_repeat(' ', intval(round($pad, 0, 2) + $this->style->getPaddingLeftRight())),
$this->text,
str_repeat(' ', intval(round($pad, 0, 1) + $this->style->getPaddingLeftRight()))
);
$promptWidth = mb_strlen($this->text) + 4;
}

$leftFill = (int) (($promptWidth / 2) - ($buttonLength / 2));

$this->emptyRow();

$this->write(sprintf(
"%s%s%s%s%s\n",
$this->style->getColoursSetCode(),
str_repeat(' ', $this->style->getPaddingLeftRight()),
$this->text,
str_repeat(' ', $this->style->getPaddingLeftRight()),
$this->style->getColoursResetCode()
));

$this->emptyRow();

$this->write(sprintf(
"%s%s%s%s%s\n",
$this->style->getColoursSetCode(),
str_repeat(' ', $leftFill),
$buttonRow,
str_repeat(' ', (int) ceil($promptWidth - $leftFill - $buttonLength)),
$this->style->getColoursResetCode()
));

$this->emptyRow();

$this->terminal->moveCursorToTop();
}
}
13 changes: 11 additions & 2 deletions src/Dialogue/Dialogue.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ abstract class Dialogue
*/
protected $text;

/**
* @var bool $cancellable
*/
protected $cancellable;

/**
* @var int
*/
Expand All @@ -42,8 +47,12 @@ abstract class Dialogue
*/
protected $y;

public function __construct(CliMenu $parentMenu, MenuStyle $menuStyle, Terminal $terminal, string $text)
{
public function __construct(
CliMenu $parentMenu,
MenuStyle $menuStyle,
Terminal $terminal,
string $text
) {
$this->style = $menuStyle;
$this->terminal = $terminal;
$this->text = $text;
Expand Down
2 changes: 1 addition & 1 deletion src/Input/InputIO.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ function (string $line) {
},
$lines
)
);
) ? : 0;
}

private function calculateYPosition() : int
Expand Down
19 changes: 19 additions & 0 deletions src/MenuStyle.php
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ private function generatePaddingTopBottomRows() : void
);
}

$this->paddingTopBottom = $this->paddingTopBottom >= 0 ? $this->paddingTopBottom : 0;
$this->paddingTopBottomRows = array_fill(0, $this->paddingTopBottom, $paddingRow);
}

Expand Down Expand Up @@ -663,6 +664,10 @@ private function generateBorderRows() : void
);
}

$this->borderTopWidth = $this->borderTopWidth >= 0 ? $this->borderTopWidth : 0;
$this->borderBottomWidth = $this->borderBottomWidth >= 0 ? $this->borderBottomWidth : 0;


$this->borderTopRows = array_fill(0, $this->borderTopWidth, $borderRow);
$this->borderBottomRows = array_fill(0, $this->borderBottomWidth, $borderRow);
}
Expand Down Expand Up @@ -813,6 +818,20 @@ public function getBorderColourCode() : string
return sprintf("\033[%sm", $borderColourCode);
}


/**
* Get ansi escape sequence for setting or unsetting the specified option code.
*
* @param string $string Option code (bold|dim|underscore|blink|reverse|conceal)
* @param bool $set Whether to set or unset the code
*
* @return string
*/
public function getOptionCode(string $string, bool $set = true): string
{
return sprintf("\033[%sm", self::$availableOptions[$string][$set ? 'set' : 'unset']);
}

/**
* Get a string of given length consisting of 0-9
* eg $length = 15 : 012345678901234
Expand Down
121 changes: 121 additions & 0 deletions test/Dialogue/ConfirmTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,54 @@ public function testConfirmWithOddLengthConfirmAndEvenLengthButton() : void
static::assertStringEqualsFile($this->getTestFile(), $this->output->fetch());
}

public function testConfirmCancellableWithShortPrompt(): void
{
$this->terminal
->method('read')
->will($this->onConsecutiveCalls(
"\n",
"\n"
));

$style = $this->getStyle($this->terminal);

$item = new SelectableItem('Item 1', function (CliMenu $menu) {
$menu->cancellableConfirm('PHP', null, true)
->display('OK', 'Cancel');

$menu->close();
});

$menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style);
$menu->open();

static::assertStringEqualsFile($this->getTestFile(), $this->output->fetch());
}

public function testConfirmCancellableWithLongPrompt(): void
{
$this->terminal
->method('read')
->will($this->onConsecutiveCalls(
"\n",
"\n"
));

$style = $this->getStyle($this->terminal);

$item = new SelectableItem('Item 1', function (CliMenu $menu) {
$menu->cancellableConfirm('PHP School Rocks FTW!', null, true)
->display('OK', 'Cancel');

$menu->close();
});

$menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style);
$menu->open();

static::assertStringEqualsFile($this->getTestFile(), $this->output->fetch());
}

public function testConfirmCanOnlyBeClosedWithEnter() : void
{
$this->terminal
Expand All @@ -166,6 +214,79 @@ public function testConfirmCanOnlyBeClosedWithEnter() : void
static::assertStringEqualsFile($this->getTestFile(), $this->output->fetch());
}

public function testConfirmOkNonCancellableReturnsTrue()
{
$this->terminal
->method('read')
->will($this->onConsecutiveCalls(
"\n",
'tab',
"\n"
));

$style = $this->getStyle($this->terminal);

$return = '';

$item = new SelectableItem('Item 1', function (CliMenu $menu) use (&$return) {
$return = $menu->cancellableConfirm('PHP School FTW!')
->display('OK');

$menu->close();
});

$menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style);
$menu->open();

static::assertTrue($return);
}

public function testConfirmOkCancellableReturnsTrue()
{
$this->terminal
->method('read')
->willReturn("\n", "\t", "\t", "\n");

$style = $this->getStyle($this->terminal);

$return = '';

$item = new SelectableItem('Item 1', function (CliMenu $menu) use (&$return) {
$return = $menu->cancellableConfirm('PHP School FTW!')
->display('OK', 'Cancel');

$menu->close();
});

$menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style);
$menu->open();

static::assertTrue($return);
}

public function testConfirmCancelCancellableReturnsFalse()
{
$this->terminal
->method('read')
->willReturn("\n", "\t", "\n");

$style = $this->getStyle($this->terminal);

$return = '';

$item = new SelectableItem('Item 1', function (CliMenu $menu) use (&$return) {
$return = $menu->cancellableConfirm('PHP School FTW!', null)
->display('OK', 'Cancel');

$menu->close();
});

$menu = new CliMenu('PHP School FTW', [$item], $this->terminal, $style);
$menu->open();

static::assertFalse($return);
}

private function getTestFile() : string
{
return sprintf('%s/../res/%s.txt', __DIR__, $this->getName());
Expand Down
Loading