diff --git a/example/CLI/tasks/MenuTask.php b/example/CLI/tasks/MenuTask.php index d3e1619..3f81063 100644 --- a/example/CLI/tasks/MenuTask.php +++ b/example/CLI/tasks/MenuTask.php @@ -16,7 +16,7 @@ class MenuTask extends Task { public function run() { - $count = $this->io->ask('Item counts of menu? [10] ', function ($value) { + $count = $this->io->ask('Item counts of menu? [10]', function ($value) { return true === (bool) preg_match('/^\d+$/', $value) || '' === $value; }); @@ -30,7 +30,7 @@ public function run() $count = 10; } - $lines = $this->io->ask('Display lines of menu? [3] ', function ($value) { + $lines = $this->io->ask('Display lines of menu? [3]', function ($value) { return true === (bool) preg_match('/^\d+$/', $value) || '' === $value; }); @@ -54,9 +54,7 @@ public function run() $list[] = sprintf('[%3d]', $index) . ' ' . md5($index); } - $this->io->writeln("Select Index"); - - $index = $this->io->menuSelect($list, $lines); + $index = $this->io->menuSelector("Select Index", $list, $lines); $this->io->log("You selected index is {$index}!"); } diff --git a/example/CLI/tasks/ReadTask.php b/example/CLI/tasks/ReadTask.php index f7543ae..1b92eb4 100644 --- a/example/CLI/tasks/ReadTask.php +++ b/example/CLI/tasks/ReadTask.php @@ -16,20 +16,18 @@ class ReadTask extends Task { public function run() { - $this->io->writeln("What is your gender?"); - - $gender = $this->io->menuSelect([ + $gender = $this->io->menuSelector('What is your gender?', [ 'male', 'female', 'other' ]); - // $this->io->write("What is your name? "); + // $this->io->write("What is your name?"); // $name = $this->io->read(); // or - $name = $this->io->ask('What is your name? ', function ($value) { + $name = $this->io->ask('What is your name?', function ($value) { return '' !== $value; }); diff --git a/src/Oni/CLI/IO.php b/src/Oni/CLI/IO.php index 47ef304..ba28fb2 100644 --- a/src/Oni/CLI/IO.php +++ b/src/Oni/CLI/IO.php @@ -82,6 +82,21 @@ private function __construct() } } } + + // Set Ctrl Handler (PHP 7 >= 7.4.0, PHP 8) + // sapi_windows_set_ctrl_handler(function () { + // switch ($event) { + // case PHP_WINDOWS_EVENT_CTRL_C: + + // // Set Cursor is Show + // $this->writeln(AEC::cursorShow()); + + // // Remove Readline Callback + // readline_callback_handler_remove(); + + // break; + // } + // }); } /** @@ -227,121 +242,145 @@ public function ask(string $text, ?callable $callback = null, ?string $fgColor = } do { - $this->write($text, $fgColor, $bgColor); + $this->write("{$text}\n> ", $fgColor, $bgColor); } while (false === $callback($answer = $this->read())); return $answer; } /** - * Menu Select + * Menu Selector * + * @param array $text * @param array $options + * @param int $specifyLimitLines * * @return int */ - public function menuSelect(array $options, ?int $currentDisplayLines = null): int + public function menuSelector(string $text, array $options, ?int $specifyLimitLines = null): int { $totalIndex = count($options); - $skipIndex = 0; - $selectedIndex = 0; - $isBreakLoop = false; - $isFirstLoop = true; - $char = null; + + if (0 === $totalIndex) { + return null; + } + + $offsetIndex = 0; + $currentIndex = 0; + $isSelectIndex = false; $wWidth = (int) exec('tput cols'); $wHeight = (int) exec('tput lines'); - $displayLines = $totalIndex <= $wHeight + $limitLines = $totalIndex <= $wHeight ? $totalIndex : $wHeight; - if (true === is_integer($currentDisplayLines) - && $currentDisplayLines > 0 - && $currentDisplayLines < $displayLines + if (true === is_integer($specifyLimitLines) + && $specifyLimitLines > 0 + && $specifyLimitLines < $limitLines ) { - $displayLines = $currentDisplayLines; + $limitLines = $specifyLimitLines; } + $this->writeln($text); + + // Install Readline Callback readline_callback_handler_install('', function() {}); // Set Cursor is Hide $this->write(AEC::cursorHide()); - do { - switch (ord($char)) { - case AEC::KEY_CODE_ENTER: - $isBreakLoop = true; + while (true) { - break; - case AEC::KEY_CODE_UP: - $selectedIndex--; + // Print Menu + $list = []; - break; - case AEC::KEY_CODE_DOWN: - $selectedIndex++; + foreach (array_slice($options, $offsetIndex, $limitLines) as $index => $option) { + $cursor = (($currentIndex - $offsetIndex) === $index) ? '> ' : ' '; + $list[] = AEC::CSI . "2K{$cursor}{$option}"; + } - break; - case AEC::KEY_CODE_PAGE_UP: - $selectedIndex -= $displayLines; + $this->write(implode("\n", $list)); - break; - case AEC::KEY_CODE_PAGE_DOWN: - $selectedIndex += $displayLines; + // Wait Typing + $isMatched = false; - break; - case AEC::KEY_CODE_HOME: - $selectedIndex = 0; + while ($char = stream_get_contents(STDIN, 1)) { + switch (ord($char)) { + case AEC::KEY_CODE_ENTER: + $isSelectIndex = true; + $isMatched = true; - break; - case AEC::KEY_CODE_END: - $selectedIndex = $totalIndex; + break; + case AEC::KEY_CODE_UP: + $currentIndex--; + $isMatched = true; - break; - } + break; + case AEC::KEY_CODE_DOWN: + $currentIndex++; + $isMatched = true; + + break; + case AEC::KEY_CODE_PAGE_UP: + $currentIndex -= $limitLines; + $isMatched = true; + + break; + case AEC::KEY_CODE_PAGE_DOWN: + $currentIndex += $limitLines; + $isMatched = true; + + break; + case AEC::KEY_CODE_HOME: + $currentIndex = 0; + $isMatched = true; - if ($selectedIndex < 0) { - $selectedIndex = 0; - } elseif ($selectedIndex >= $totalIndex) { - $selectedIndex = $totalIndex - 1; + break; + case AEC::KEY_CODE_END: + $currentIndex = $totalIndex; + $isMatched = true; + + break; + } + + if (true === $isMatched) { + break; + } } - if (true === $isBreakLoop) { + if (true === $isSelectIndex) { break; } - // Set Cursor Prev - if (false === $isFirstLoop) { - $this->write(AEC::cursorPrev($displayLines - 1)); - } else { - $isFirstLoop = false; + // Set Selected Index + if ($currentIndex < 0) { + $currentIndex = 0; + } elseif ($currentIndex >= $totalIndex) { + $currentIndex = $totalIndex - 1; } // Set Skip Index - if ($selectedIndex < $skipIndex) { - $skipIndex = $selectedIndex; - } else if ($selectedIndex > $skipIndex + $displayLines - 1) { - $skipIndex = $selectedIndex - $displayLines + 1; + if ($currentIndex < $offsetIndex) { + $offsetIndex = $currentIndex; + } else if ($currentIndex > $offsetIndex + $limitLines - 1) { + $offsetIndex = $currentIndex - $limitLines + 1; } - // Get Current Options - $currentOptions = array_slice($options, $skipIndex, $displayLines); - - // Print Menu - $this->write(implode("\n", array_map(function ($option, $currentIndex) use ($selectedIndex, $skipIndex) { - $padding = (($selectedIndex - $skipIndex) === $currentIndex) - ? '> ' : ' '; - - return AEC::CSI . "2K{$padding}{$option}"; - }, $currentOptions, array_keys($currentOptions)))); - - } while ($char = stream_get_contents(STDIN, 1)); + if (1 < $limitLines) { + $this->write(AEC::cursorPrev($limitLines - 1)); // Set Cursor Prev + } else { + $this->write("\r"); // Set Return + } + } // Set Cursor is Show $this->writeln(AEC::cursorShow()); + // Remove Readline Callback readline_callback_handler_remove(); - return $selectedIndex; + return $currentIndex; } /**