From ce1e7f5b6bef201cbf76d51930937937b5b3f601 Mon Sep 17 00:00:00 2001 From: inhere Date: Fri, 11 Jun 2021 12:02:49 +0800 Subject: [PATCH] feat: add an new simple interactive shell componnent --- src/AbstractApplication.php | 4 +- src/Component/Interact/AbstractQuestion.php | 43 +++ src/Component/Interact/Checkbox.php | 6 +- src/Component/Interact/Choose.php | 8 +- src/Component/Interact/Confirm.php | 4 +- src/Component/Interact/IShell.php | 336 ++++++++++++++++++++ src/Component/Interact/LimitedAsk.php | 12 +- src/Component/Interact/Password.php | 4 +- src/Component/Interact/Question.php | 4 +- src/Component/Interact/Terminal.php | 4 +- src/Component/InteractMessage.php | 12 - src/Component/InteractiveHandle.php | 51 +++ 12 files changed, 453 insertions(+), 35 deletions(-) create mode 100644 src/Component/Interact/AbstractQuestion.php create mode 100644 src/Component/Interact/IShell.php delete mode 100644 src/Component/InteractMessage.php create mode 100644 src/Component/InteractiveHandle.php diff --git a/src/AbstractApplication.php b/src/AbstractApplication.php index e37c101..61e73cd 100644 --- a/src/AbstractApplication.php +++ b/src/AbstractApplication.php @@ -428,8 +428,8 @@ protected function startInteractiveShell(): void $this->debugf('php is not enable "pcntl" extension, cannot listen CTRL+C signal'); } + // register signal. if ($hasPcntl) { - // register signal. ProcessUtil::installSignal(Signal::INT, static function () use ($out) { $out->colored("\nQuit by CTRL+C"); exit(0); @@ -460,8 +460,8 @@ protected function startInteractiveShell(): void } } + // listen signal. if ($hasPcntl) { - // listen signal. ProcessUtil::dispatchSignal(); } diff --git a/src/Component/Interact/AbstractQuestion.php b/src/Component/Interact/AbstractQuestion.php new file mode 100644 index 0000000..70e8070 --- /dev/null +++ b/src/Component/Interact/AbstractQuestion.php @@ -0,0 +1,43 @@ +answer = StrObject::new($str)->trim(); + } + + /** + * @return StrObject + */ + public function getAnswer(): StrObject + { + return $this->answer; + } + + /** + * @return int + */ + public function getInt(): int + { + return $this->answer->toInt(); + } +} diff --git a/src/Component/Interact/Checkbox.php b/src/Component/Interact/Checkbox.php index 70ab6c9..ba2f8df 100644 --- a/src/Component/Interact/Checkbox.php +++ b/src/Component/Interact/Checkbox.php @@ -2,7 +2,7 @@ namespace Inhere\Console\Component\Interact; -use Inhere\Console\Component\InteractMessage; +use Inhere\Console\Component\InteractiveHandle; use Inhere\Console\Console; use Inhere\Console\Util\Show; use function array_filter; @@ -17,7 +17,7 @@ * * @package Inhere\Console\Component\Interact */ -class Checkbox extends InteractMessage +class Checkbox extends InteractiveHandle { /** * List multiple options and allow multiple selections @@ -29,7 +29,7 @@ class Checkbox extends InteractMessage * * @return array */ - public static function select(string $description, $options, $default = null, $allowExit = true): array + public static function select(string $description, $options, $default = null, bool $allowExit = true): array { if (!$description = trim($description)) { Show::error('Please provide a description text!', 1); diff --git a/src/Component/Interact/Choose.php b/src/Component/Interact/Choose.php index 0ea454b..10a0b5a 100644 --- a/src/Component/Interact/Choose.php +++ b/src/Component/Interact/Choose.php @@ -2,7 +2,7 @@ namespace Inhere\Console\Component\Interact; -use Inhere\Console\Component\InteractMessage; +use Inhere\Console\Component\InteractiveHandle; use Inhere\Console\Console; use Inhere\Console\Util\Show; use function array_key_exists; @@ -15,7 +15,7 @@ * * @package Inhere\Console\Component\Interact */ -class Choose extends InteractMessage +class Choose extends InteractiveHandle { /** * Choose one of several options @@ -55,11 +55,11 @@ public static function one(string $description, $options, $default = null, bool $text .= "\n $key) $value"; } - $defaultText = $default ? "[default:{$default}]" : ''; + $defaultText = $default ? "[default:$default]" : ''; Console::write($text); beginChoice: - $r = Console::readln("Your choice{$defaultText} : "); + $r = Console::readln("Your choice$defaultText : "); // error, allow try again once. if (!array_key_exists($r, $options)) { diff --git a/src/Component/Interact/Confirm.php b/src/Component/Interact/Confirm.php index 95023ed..955ea14 100644 --- a/src/Component/Interact/Confirm.php +++ b/src/Component/Interact/Confirm.php @@ -2,7 +2,7 @@ namespace Inhere\Console\Component\Interact; -use Inhere\Console\Component\InteractMessage; +use Inhere\Console\Component\InteractiveHandle; use Inhere\Console\Console; use Inhere\Console\Util\Show; use function stripos; @@ -14,7 +14,7 @@ * * @package Inhere\Console\Component\Interact */ -class Confirm extends InteractMessage +class Confirm extends InteractiveHandle { /** * Send a message request confirmation diff --git a/src/Component/Interact/IShell.php b/src/Component/Interact/IShell.php new file mode 100644 index 0000000..71dc230 --- /dev/null +++ b/src/Component/Interact/IShell.php @@ -0,0 +1,336 @@ + 1, + 'quit' => 1, + 'exit' => 1, + ]; + + /** + * @param array $options + * + * @return static + */ + public static function new(array $options = []): self + { + return new self($options); + } + + /** + * Quick create and start run an shell + * + * @param callable $handler + * @param array $options + * - title + * - prefix + */ + public static function run(callable $handler, array $options = []): void + { + $options['handler'] = $handler; + + (new self($options))->start(); + } + + /** + * @return bool + */ + protected function registerSignal(): bool + { + if (!($hasPcntl = ProcessUtil::hasPcntl())) { + $this->debugf('php is not enable "pcntl" extension, cannot listen CTRL+C signal'); + } + + // register signal. + if ($hasPcntl) { + ProcessUtil::installSignal(Signal::INT, static function () { + Show::colored("\nQuit by CTRL+C"); + exit(0); + }); + } + + return $hasPcntl; + } + + protected function beforeStart(): void + { + if ($this->title) { + Title::show($this->title, [ + 'titlePos' => Title::POS_MIDDLE, + ]); + } + + if (!$this->errorHandler) { + $this->errorHandler = $this->defaultErrorHandler(); + } + } + + /** + * Start shell to run + */ + public function start(): void + { + if (!$handler = $this->handler) { + throw new RuntimeException('must be set the logic handler for start'); + } + + $prefix = $this->prefix; + $this->beforeStart(); + + $hasPcntl = $this->registerSignal(); + while (true) { + $line = Interact::readln("$prefix> "); + + // listen signal. + if ($hasPcntl) { + ProcessUtil::dispatchSignal(); + } + + $state = $this->dispatch($line, $handler); + if ($state === self::STOP) { + break; + } + + Console::println(''); + } + + Show::colored("\nQuit. ByeBye!"); + } + + /** + * @param string $line + * @param callable $handler + * + * @return int + * @throws RuntimeException + */ + protected function dispatch(string $line, callable $handler): int + { + if (strlen($line) < 5) { + // exit + if (isset($this->exitKeys[$line])) { + return self::STOP; + } + + // "?" as show help + if ($line === '?') { + $line = self::HELP; + } + } + + // display help + $hasMoreKey = false; + if ($line === self::HELP || ($hasMoreKey = strpos($line, 'help ') === 0)) { + // help CMD + $moreKeys = $hasMoreKey ? explode(' ', $line) : []; + $this->handleHelp($moreKeys); + return self::GOON; + } + + $this->debugf('input line: %s', $line); + + try { + // call validator + if ($vfn = $this->validator) { + $line = $vfn($line); + } + + $handler($line); + } catch (Throwable $e) { + if ($fn = $this->errorHandler) { + $fn($e); + } else { + throw new RuntimeException('dispatch error, line: ' . $line, 500, $e); + } + } + + return self::GOON; + } + + /** + * @param array $moreKeys + */ + protected function handleHelp(array $moreKeys): void + { + if ($fn = $this->helpHandler) { + $fn($moreKeys); + return; + } + + Console::println('no help message'); + } + + /** + * @return Closure + */ + public function emptyValidator(): Closure + { + return static function (string $line) { + if ($line === '') { + throw new InvalidArgumentException('input is empty!'); + } + return $line; + }; + } + + /** + * @return Closure + */ + public function defaultErrorHandler(): Closure + { + return static function (Throwable $e) { + Console::write('ERROR: ' . $e->getMessage(), false); + }; + } + + /** + * @param string $format + * @param mixed ...$args + */ + public function debugf(string $format, ...$args): void + { + if ($this->debug) { + Console::logf(Console::VERB_DEBUG, $format, ...$args); + } + } + + /** + * @param callable $handler + * + * @return IShell + */ + public function setHandler(callable $handler): IShell + { + $this->handler = $handler; + return $this; + } + + /** + * @param bool $debug + * + * @return IShell + */ + public function setDebug(bool $debug): IShell + { + $this->debug = $debug; + return $this; + } + + /** + * @param array $exitKeys + * + * @return IShell + */ + public function setExitKeys(array $exitKeys): IShell + { + $this->exitKeys = $exitKeys; + return $this; + } + + /** + * @param string $title + * + * @return IShell + */ + public function setTitle(string $title): IShell + { + $this->title = $title; + return $this; + } + + /** + * @param string $prefix + * + * @return IShell + */ + public function setPrefix(string $prefix): IShell + { + $this->prefix = $prefix; + return $this; + } + + /** + * @param callable $helpHandler + * + * @return IShell + */ + public function setHelpHandler(callable $helpHandler): IShell + { + $this->helpHandler = $helpHandler; + return $this; + } + + /** + * @param callable $errorHandler + * + * @return IShell + */ + public function setErrorHandler(callable $errorHandler): IShell + { + $this->errorHandler = $errorHandler; + return $this; + } +} diff --git a/src/Component/Interact/LimitedAsk.php b/src/Component/Interact/LimitedAsk.php index 21d1bbf..9f792d4 100644 --- a/src/Component/Interact/LimitedAsk.php +++ b/src/Component/Interact/LimitedAsk.php @@ -3,7 +3,7 @@ namespace Inhere\Console\Component\Interact; use Closure; -use Inhere\Console\Component\InteractMessage; +use Inhere\Console\Component\InteractiveHandle; use Inhere\Console\Console; use Inhere\Console\Util\Show; use function sprintf; @@ -15,17 +15,17 @@ * * @package Inhere\Console\Component\Interact */ -class LimitedAsk extends InteractMessage +class LimitedAsk extends InteractiveHandle { /** * Ask a question, ask for a limited number of times * 若输入了值且验证成功则返回 输入的结果 * 否则,会连续询问 $times 次, 若仍然错误,退出 * - * @param string $question 问题 - * @param string $default 默认值 - * @param Closure $validator (默认验证输入是否为空)自定义回调验证输入是否符合要求; 验证成功返回true 否则 可返回错误消息 - * @param int $times Allow input times + * @param string $question 问题 + * @param string $default 默认值 + * @param Closure|null $validator (默认验证输入是否为空)自定义回调验证输入是否符合要求; 验证成功返回true 否则 可返回错误消息 + * @param int $times Allow input times * * @return string * @example This is an example diff --git a/src/Component/Interact/Password.php b/src/Component/Interact/Password.php index 681ea10..08647c2 100644 --- a/src/Component/Interact/Password.php +++ b/src/Component/Interact/Password.php @@ -2,7 +2,7 @@ namespace Inhere\Console\Component\Interact; -use Inhere\Console\Component\InteractMessage; +use Inhere\Console\Component\InteractiveHandle; use RuntimeException; use Toolkit\Sys\Sys; use function addslashes; @@ -18,7 +18,7 @@ * * @package Inhere\Console\Component\Interact */ -class Password extends InteractMessage +class Password extends InteractiveHandle { /** * Interactively prompts for input without echoing to the terminal. diff --git a/src/Component/Interact/Question.php b/src/Component/Interact/Question.php index 9af679c..9f74ed6 100644 --- a/src/Component/Interact/Question.php +++ b/src/Component/Interact/Question.php @@ -3,7 +3,7 @@ namespace Inhere\Console\Component\Interact; use Closure; -use Inhere\Console\Component\InteractMessage; +use Inhere\Console\Component\InteractiveHandle; use Inhere\Console\Console; use Inhere\Console\Util\Show; use function trim; @@ -14,7 +14,7 @@ * * @package Inhere\Console\Component\Interact */ -class Question extends InteractMessage +class Question extends InteractiveHandle { /** * Ask a question, ask for results; return the result of the input diff --git a/src/Component/Interact/Terminal.php b/src/Component/Interact/Terminal.php index 9f961ac..3acdd0f 100644 --- a/src/Component/Interact/Terminal.php +++ b/src/Component/Interact/Terminal.php @@ -2,13 +2,13 @@ namespace Inhere\Console\Component\Interact; -use Inhere\Console\Component\InteractMessage; +use Inhere\Console\Component\InteractiveHandle; /** * Class Terminal * * @package Inhere\Console\Component\Interact */ -class Terminal extends InteractMessage +class Terminal extends InteractiveHandle { } diff --git a/src/Component/InteractMessage.php b/src/Component/InteractMessage.php deleted file mode 100644 index c18dbfd..0000000 --- a/src/Component/InteractMessage.php +++ /dev/null @@ -1,12 +0,0 @@ -setValidator(function (string $line) { + * // check input + * if (!$line) { + * throw new InvalidArgumentException('argument is required'); + * } + * return $line; + * }); + * ``` + * + * @var callable + */ + protected $validator; + + /** + * Class constructor. + * + * @param array $options + */ + public function __construct(array $options = []) + { + Obj::init($this, $options); + } + + /** + * @param callable $validator + * + * @return self + */ + public function setValidator(callable $validator): self + { + $this->validator = $validator; + return $this; + } +}