diff --git a/herrfristi/laravel-output-writer/composer.json b/herrfristi/laravel-output-writer/composer.json new file mode 100644 index 0000000..e69de29 diff --git a/herrfristi/laravel-output-writer/src/Console/Command/BaseCommand.php b/herrfristi/laravel-output-writer/src/Console/Command/BaseCommand.php new file mode 100644 index 0000000..77d0ee9 --- /dev/null +++ b/herrfristi/laravel-output-writer/src/Console/Command/BaseCommand.php @@ -0,0 +1,54 @@ + + */ + +namespace Arc\Base\Console\Command; + + +use Arc\Base\Console\Output\BaseOutputWriter; +use Arc\Base\Console\Output\OutputWriterInterface; +use Illuminate\Console\Command; +use Illuminate\Console\OutputStyle; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Class BaseCommand + * + * @package Arc\Base\Console\Command + * @author Marvin Schreurs + */ +abstract class BaseCommand extends Command +{ + /** + * OutputWriterInterface instance. + * + * @var OutputWriterInterface $writer + */ + protected $writer; + + /** + * @inheritDoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->setOutputWriter($input, $output); + return parent::execute($input, $output); + } + + /** + * Set the output writer to use with this command. + */ + protected function setOutputWriter(InputInterface $input, OutputInterface $output): void + { + $this->writer = new BaseOutputWriter($input, $output); + } +} \ No newline at end of file diff --git a/herrfristi/laravel-output-writer/src/Console/Helper/QuestionHelper.php b/herrfristi/laravel-output-writer/src/Console/Helper/QuestionHelper.php new file mode 100644 index 0000000..1aae1fb --- /dev/null +++ b/herrfristi/laravel-output-writer/src/Console/Helper/QuestionHelper.php @@ -0,0 +1,149 @@ + + */ + +namespace Arc\Base\Console\Helper; + + +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\StreamableInputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Class QuestionHelper + * + * @package Arc\Base\Console\Helper + * @author Marvin Schreurs + */ +class QuestionHelper extends UnfuckedQuestionHelper +{ + public function ask(InputInterface $input, OutputInterface $output, Question $question) + { + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + if (!$input->isInteractive()) { + $default = $question->getDefault(); + + if (null === $default) { + return $default; + } + + if ($validator = $question->getValidator()) { + return \call_user_func($question->getValidator(), $default); + } elseif ($question instanceof ChoiceQuestion) { + if (!$question->isMultiselect()) { + return $default; + } + + $default = explode(',', $default); + foreach ($default as $k => $v) { + $v = trim($v); + $default[$k] = $v; + } + } + + return $default; + } + + if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) { + $this->inputStream = $stream; + } + + if (!$question->getValidator()) { + return $this->doAsk($output, $question); + } + + $interviewer = function () use ($output, $question) { + return $this->doAsk($output, $question); + }; + + return $this->validateAttempts($interviewer, $output, $question); + } + + /** + * {@inheritdoc} + */ + protected function writePrompt(OutputInterface $output, Question $question) + { + $text = OutputFormatter::escapeTrailingBackslash($question->getQuestion()); + $default = $question->getDefault(); + + switch (true) { + case null === $default: + $text = sprintf(' %s:', $text); + + break; + + case $question instanceof ConfirmationQuestion: + $text = sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no'); + + break; + + case $question instanceof ChoiceQuestion && $question->isMultiselect(): + $choices = $question->getChoices(); + $default = explode(',', $default); + + foreach ($default as $key => $value) { + $default[$key] = $choices[trim($value)]; + } + + $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape(implode(', ', $default))); + + break; + + case $question instanceof ChoiceQuestion: + $choices = $question->getChoices(); + $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape(isset($choices[$default]) ? $choices[$default] : $default)); + + break; + + default: + $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape($default)); + } + + $output->writeln($text); + + if ($question instanceof ChoiceQuestion) { + $width = max(array_map('strlen', array_keys($question->getChoices()))); + + foreach ($question->getChoices() as $key => $value) { + $output->writeln(sprintf(" [%-${width}s] %s", $key, $value)); + } + } + + $output->write(' > '); + } + + /** + * {@inheritdoc} + */ + protected function writeError(OutputInterface $output, \Exception $error) + { + if ($output instanceof SymfonyStyle) { + $output->newLine(); + $output->error($error->getMessage()); + + return; + } + + parent::writeError($output, $error); + } +} \ No newline at end of file diff --git a/herrfristi/laravel-output-writer/src/Console/Helper/UnfuckedQuestionHelper.php b/herrfristi/laravel-output-writer/src/Console/Helper/UnfuckedQuestionHelper.php new file mode 100644 index 0000000..1c7c352 --- /dev/null +++ b/herrfristi/laravel-output-writer/src/Console/Helper/UnfuckedQuestionHelper.php @@ -0,0 +1,485 @@ + + */ + +namespace Arc\Base\Console\Helper; + + +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\StreamableInputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\Question; + +/** + * Class UnfuckedQuestionHelper + * + * @package Arc\Base\Console\Helper + * @author Marvin Schreurs + */ +class UnfuckedQuestionHelper extends \Symfony\Component\Console\Helper\QuestionHelper +{ + protected $inputStream; + protected static $shell; + protected static $stty; + + /** + * Asks a question to the user. + * + * @return mixed The user answer + * + * @throws RuntimeException If there is no data to read in the input stream + * @throws \Exception + */ + public function ask(InputInterface $input, OutputInterface $output, Question $question) + { + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + if (!$input->isInteractive()) { + $default = $question->getDefault(); + + if (null === $default) { + return $default; + } + + if ($validator = $question->getValidator()) { + return \call_user_func($question->getValidator(), $default); + } elseif ($question instanceof ChoiceQuestion) { + $choices = $question->getChoices(); + + if (!$question->isMultiselect()) { + return isset($choices[$default]) ? $choices[$default] : $default; + } + + $default = explode(',', $default); + foreach ($default as $k => $v) { + $v = trim($v); + $default[$k] = isset($choices[$v]) ? $choices[$v] : $v; + } + } + + return $default; + } + + if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) { + $this->inputStream = $stream; + } + + if (!$question->getValidator()) { + return $this->doAsk($output, $question); + } + + $interviewer = function () use ($output, $question) { + return $this->doAsk($output, $question); + }; + + return $this->validateAttempts($interviewer, $output, $question); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'question'; + } + + /** + * Prevents usage of stty. + */ + public static function disableStty() + { + static::$stty = false; + } + + /** + * Asks the question to the user. + * + * @return bool|mixed|string|null + * + * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden + */ + protected function doAsk(OutputInterface $output, Question $question) + { + $this->writePrompt($output, $question); + + $inputStream = $this->inputStream ?: STDIN; + $autocomplete = $question->getAutocompleterCallback(); + + if (null === $autocomplete || !$this->hasSttyAvailable()) { + $ret = false; + if ($question->isHidden()) { + try { + $ret = trim($this->getHiddenResponse($output, $inputStream)); + } catch (RuntimeException $e) { + if (!$question->isHiddenFallback()) { + throw $e; + } + } + } + + if (false === $ret) { + $ret = fgets($inputStream, 4096); + if (false === $ret) { + throw new RuntimeException('Aborted.'); + } + $ret = trim($ret); + } + } else { + $ret = trim($this->autocomplete($output, $question, $inputStream, $autocomplete)); + } + + if ($output instanceof ConsoleSectionOutput) { + $output->addContent($ret); + } + + $ret = \strlen($ret) > 0 ? $ret : $question->getDefault(); + + if ($normalizer = $question->getNormalizer()) { + return $normalizer($ret); + } + + return $ret; + } + + /** + * Outputs the question prompt. + */ + protected function writePrompt(OutputInterface $output, Question $question) + { + $message = $question->getQuestion(); + + if ($question instanceof ChoiceQuestion) { + $maxWidth = max(array_map([$this, 'strlen'], array_keys($question->getChoices()))); + + $messages = (array) $question->getQuestion(); + foreach ($question->getChoices() as $key => $value) { + $width = $maxWidth - $this->strlen($key); + $messages[] = ' ['.$key.str_repeat(' ', $width).'] '.$value; + } + + $output->writeln($messages); + + $message = $question->getPrompt(); + } + + $output->write($message); + } + + /** + * Outputs an error message. + */ + protected function writeError(OutputInterface $output, \Exception $error) + { + if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) { + $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'); + } else { + $message = ''.$error->getMessage().''; + } + + $output->writeln($message); + } + + /** + * Autocompletes a question. + * + * @param resource $inputStream + * @return string + */ + private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string + { + $fullChoice = ''; + $ret = ''; + + $i = 0; + $ofs = -1; + $matches = $autocomplete($ret); + $numMatches = \count($matches); + + $sttyMode = shell_exec('stty -g'); + + // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) + shell_exec('stty -icanon -echo'); + + // Add highlighted text style + $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white')); + + // Read a keypress + while (!feof($inputStream)) { + $c = fread($inputStream, 1); + + // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false. + if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) { + shell_exec(sprintf('stty %s', $sttyMode)); + throw new RuntimeException('Aborted.'); + } elseif ("\177" === $c) { // Backspace Character + if (0 === $numMatches && 0 !== $i) { + --$i; + $fullChoice = substr($fullChoice, 0, -1); + // Move cursor backwards + $output->write("\033[1D"); + } + + if (0 === $i) { + $ofs = -1; + $matches = $autocomplete($ret); + $numMatches = \count($matches); + } else { + $numMatches = 0; + } + + // Pop the last character off the end of our string + $ret = substr($ret, 0, $i); + } elseif ("\033" === $c) { + // Did we read an escape sequence? + $c .= fread($inputStream, 2); + + // A = Up Arrow. B = Down Arrow + if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) { + if ('A' === $c[2] && -1 === $ofs) { + $ofs = 0; + } + + if (0 === $numMatches) { + continue; + } + + $ofs += ('A' === $c[2]) ? -1 : 1; + $ofs = ($numMatches + $ofs) % $numMatches; + } + } elseif (\ord($c) < 32) { + if ("\t" === $c || "\n" === $c) { + if ($numMatches > 0 && -1 !== $ofs) { + $ret = (string) $matches[$ofs]; + // Echo out remaining chars for current match + $remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)))); + $output->write($remainingCharacters); + $fullChoice .= $remainingCharacters; + $i = \strlen($fullChoice); + + $matches = array_filter( + $autocomplete($ret), + function ($match) use ($ret) { + return '' === $ret || 0 === strpos($match, $ret); + } + ); + $numMatches = \count($matches); + $ofs = -1; + } + + if ("\n" === $c) { + $output->write($c); + break; + } + } + + continue; + } else { + if ("\x80" <= $c) { + $c .= fread($inputStream, ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3][$c & "\xF0"]); + } + + $output->write($c); + $ret .= $c; + $fullChoice .= $c; + ++$i; + + $tempRet = $ret; + + if ($question instanceof ChoiceQuestion && $question->isMultiselect()) { + $tempRet = $this->mostRecentlyEnteredValue($fullChoice); + } + + $numMatches = 0; + $ofs = 0; + + foreach ($autocomplete($ret) as $value) { + // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle) + if (0 === strpos($value, $tempRet)) { + $matches[$numMatches++] = $value; + } + } + } + + // Erase characters from cursor to end of line + $output->write("\033[K"); + + if ($numMatches > 0 && -1 !== $ofs) { + // Save cursor position + $output->write("\0337"); + // Write highlighted text, complete the partially entered response + $charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))); + $output->write(''.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).''); + // Restore cursor position + $output->write("\0338"); + } + } + + // Reset stty so it behaves normally again + shell_exec(sprintf('stty %s', $sttyMode)); + + return $fullChoice; + } + + private function mostRecentlyEnteredValue($entered) + { + // Determine the most recent value that the user entered + if (false === strpos($entered, ',')) { + return $entered; + } + + $choices = explode(',', $entered); + if (\strlen($lastChoice = trim($choices[\count($choices) - 1])) > 0) { + return $lastChoice; + } + + return $entered; + } + + /** + * Gets a hidden response from user. + * + * @param OutputInterface $output An Output instance + * @param resource $inputStream The handler resource + * + * @return string + * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden + */ + private function getHiddenResponse(OutputInterface $output, $inputStream): string + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $exe = __DIR__.'/../Resources/bin/hiddeninput.exe'; + + // handle code running from a phar + if ('phar:' === substr(__FILE__, 0, 5)) { + $tmpExe = sys_get_temp_dir().'/hiddeninput.exe'; + copy($exe, $tmpExe); + $exe = $tmpExe; + } + + $value = rtrim(shell_exec($exe)); + $output->writeln(''); + + if (isset($tmpExe)) { + unlink($tmpExe); + } + + return $value; + } + + if ($this->hasSttyAvailable()) { + $sttyMode = shell_exec('stty -g'); + + shell_exec('stty -echo'); + $value = fgets($inputStream, 4096); + shell_exec(sprintf('stty %s', $sttyMode)); + + if (false === $value) { + throw new RuntimeException('Aborted.'); + } + + $value = trim($value); + $output->writeln(''); + + return $value; + } + + if (false !== $shell = $this->getShell()) { + $readCmd = 'csh' === $shell ? 'set mypassword = $<' : 'read -r mypassword'; + $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); + $value = rtrim(shell_exec($command)); + $output->writeln(''); + + return $value; + } + + throw new RuntimeException('Unable to hide the response.'); + } + + /** + * Validates an attempt. + * + * @param callable $interviewer A callable that will ask for a question and return the result + * @param OutputInterface $output An Output instance + * @param Question $question A Question instance + * + * @return mixed The validated response + * + * @throws \Exception In case the max number of attempts has been reached and no valid response has been given + */ + protected function validateAttempts(callable $interviewer, OutputInterface $output, Question $question) + { + $error = null; + $attempts = $question->getMaxAttempts(); + while (null === $attempts || $attempts--) { + if (null !== $error) { + $this->writeError($output, $error); + } + + try { + return $question->getValidator()($interviewer()); + } catch (RuntimeException $e) { + throw $e; + } catch (\Exception $error) { + } + } + + throw $error; + } + + /** + * Returns a valid unix shell. + * + * @return string|bool The valid shell name, false in case no valid shell is found + */ + protected function getShell() + { + if (null !== static::$shell) { + return static::$shell; + } + + static::$shell = false; + + if (file_exists('/usr/bin/env')) { + // handle other OSs with bash/zsh/ksh/csh if available to hide the answer + $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null"; + foreach (['bash', 'zsh', 'ksh', 'csh'] as $sh) { + if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) { + static::$shell = $sh; + break; + } + } + } + + return static::$shell; + } + + /** + * Returns whether Stty is available or not. + */ + protected function hasSttyAvailable(): bool + { + if (null !== static::$stty) { + return static::$stty; + } + + exec('stty 2>&1', $output, $exitcode); + + return static::$stty = 0 === $exitcode; + } +} \ No newline at end of file diff --git a/herrfristi/laravel-output-writer/src/Console/Output/BaseOutputWriter.php b/herrfristi/laravel-output-writer/src/Console/Output/BaseOutputWriter.php new file mode 100644 index 0000000..ff93f94 --- /dev/null +++ b/herrfristi/laravel-output-writer/src/Console/Output/BaseOutputWriter.php @@ -0,0 +1,588 @@ + + */ + +namespace Arc\Base\Console\Output; + + +use Arc\Base\Console\Helper\QuestionHelper; +use Arc\Base\Console\Question\BaseChoiceQuestion; +use Arc\Base\Console\Question\BaseConfirmationQuestion; +use Arc\Base\Console\Question\BaseQuestion; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\SymfonyQuestionHelper; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Terminal; + +/** + * Class BaseOutputWriter + * + * @package Arc\Base\Console\Output + * @author Marvin Schreurs + */ +class BaseOutputWriter implements OutputWriterInterface +{ + /** + * Console input interface. + * + * @var InputInterface $input + */ + protected $input; + + /** + * Console output interface. + * + * @var OutputInterface $output + */ + protected $output; + + /** + * ProgressBar instance. + * + * @var ProgressBar $progressBar + */ + protected $progressBar; + + /** + * Output buffer. + * + * @var BufferedOutput $bufferedOutput + */ + protected $bufferedOutput; + + /** + * Helper instance for handling questions. + * + * @var SymfonyQuestionHelper $questionHelper + */ + protected $questionHelper; + + /** + * Calculated line length that will be used in the console. + * + * @var int $lineLength + */ + protected $lineLength; + + /** + * The list of available output styles with their settings. + * + * @var array $styles + */ + protected $styles = [ + 'comment' => ['white', 'black'], + 'info' => ['cyan', 'black'], + 'warning' => ['yellow', 'black'], + 'danger' => ['red', 'black'], + 'success' => ['green', 'black'], + 'comment-bg' => ['black', 'white'], + 'info-bg' => ['black', 'cyan'], + 'warning-bg' => ['black', 'yellow'], + 'danger-bg' => ['white', 'red'], + 'success-bg' => ['black', 'green'], + 'title' => ['green', 'black'], + 'section' => ['white', 'black'], + 'question'=> ['green', 'black'], + 'answer' => ['yellow', 'black'], + 'choice' => ['yellow', 'black'], + ]; + + /** + * The maximum length of lines to use for console ouput. + * + * @var int $maxLineLength + */ + protected $maxLineLength = 80; + + /** + * BaseOutputStyler constructor. + * + * @param InputInterface $input + * @param OutputInterface $output + */ + public function __construct(InputInterface $input, OutputInterface $output) + { + //Set input and output interface + $this->input = $input; + $this->output = $output; + + //Set additional output styles + foreach($this->styles as $style => $values) { + $formatter = new OutputFormatterStyle($values[0], $values[1]); + $output->getFormatter()->setStyle($style, $formatter); + } + + $this->bufferedOutput = new BufferedOutput($output->getVerbosity(), false, clone $output->getFormatter()); + // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not. + $width = (new Terminal())->getWidth() ?: $this->maxLineLength; + $this->lineLength = min($width - (int) (\DIRECTORY_SEPARATOR === '\\'), $this->maxLineLength); + } + + /** + * {@inheritdoc} + */ + public function title(string $title): void + { + $this->autoPrependBlock(); + $this->writeln([ + sprintf('%s</>', OutputFormatter::escapeTrailingBackslash($title)), + sprintf('<title>%s</>', str_repeat('=', Helper::strlenWithoutDecoration($this->output->getFormatter(), $title))), + ]); + $this->newLine(); + } + + /** + * {@inheritdoc} + */ + public function section(string $heading): void + { + $this->autoPrependBlock(); + $this->writeln([ + sprintf('<section>%s</>', OutputFormatter::escapeTrailingBackslash($heading)), + sprintf('<section>%s</>', str_repeat('-', Helper::strlenWithoutDecoration($this->output->getFormatter(), $heading))), + ]); + $this->newLine(); + } + + /** + * {@inheritdoc} + */ + public function table(array $headers, array $rows): void + { + $style = clone Table::getStyleDefinition('symfony-style-guide'); + $style->setCellHeaderFormat('<info>%s</info>'); + + $table = new Table($this->output); + $table->setHeaders($headers); + $table->setRows($rows); + $table->setStyle($style); + + $table->render(); + $this->newLine(); + } + + /** + * {@inheritdoc} + */ + public function list(array $items, bool $dictionary = false): void + { + $this->autoPrependText(); + + if($dictionary) { + $width = $this->_getColumnWidth(array_keys($items)); + $this->newLine(); + foreach($items as $item => $description) { + //Format line + $spacingWidth = $width - Helper::strlen($item); + $line = sprintf(' %s%s%s', $item, str_repeat(' ', $spacingWidth), $description); + $this->writeln($line); + } + } else { + $items = array_map(function ($item) { + return sprintf(' * %s', $item); + }, $items); + $this->writeln($items); + } + + $this->newLine(); + } + + /** + * {@inheritdoc} + */ + public function text($message, string $style = null): void + { + $this->autoPrependText(); + + $messages = \is_array($message) ? array_values($message) : [$message]; + foreach ($messages as $message) { + if(!empty($style)){ + $message = sprintf(" <$style>%s</>", $message); + } + + $this->writeln(sprintf(' %s', $message)); + } + } + + /** + * {@inheritdoc} + */ + public function success($message, bool $padding = false): void + { + $style = $padding ? 'success-bg' : 'success'; + $this->block($message, 'OK', $style, ' ', $padding); + } + + /** + * {@inheritdoc} + */ + public function error($message, bool $padding = false): void + { + $style = $padding ? 'danger-bg' : 'danger'; + $this->block($message, 'ERROR', $style, ' ', $padding); + } + + /** + * {@inheritdoc} + */ + public function warning($message, bool $padding = false): void + { + $style = $padding ? 'warning-bg' : 'warning'; + $this->block($message, 'WARN', $style, ' ', $padding); + } + + /** + * {@inheritdoc} + */ + public function comment($message, bool $padding = false): void + { + $style = $padding ? 'comment-bg' : 'comment'; + $this->block($message, null, $style, " // ", $padding, false); + } + + /** + * {@inheritdoc} + */ + public function info($message, bool $padding = false): void + { + $style = $padding ? 'info-bg' : 'info'; + $this->block($message, 'INFO', $style, ' ', $padding); + } + + /** + * {@inheritdoc} + */ + public function exception(\Throwable $throwable, string $message = null): void + { + $this->error([ + $message ?? 'An exception has occurred!', + '', + sprintf('In %s on line %s:', $throwable->getFile(), $throwable->getLine()), + $throwable->getMessage() + ], true); + } + + /** + * {@inheritdoc} + */ + public function ask(string $question, $default = null, $validator = null) + { + $question = new BaseQuestion($question, $default); + $question->setValidator($validator); + + return $this->askQuestion($question); + } + + /** + * {@inheritdoc} + */ + public function password(string $question, $validator = null) + { + $question = new BaseQuestion($question); + + $question->setHidden(true); + $question->setValidator($validator); + + return $this->askQuestion($question); + } + + /** + * {@inheritdoc} + */ + public function confirm(string $question, $default = true): bool + { + return $this->askQuestion(new BaseConfirmationQuestion($question, $default)); + } + + /** + * {@inheritdoc} + */ + public function choice(string $question, array $choices, $default = null) + { + return $this->askQuestion(new BaseChoiceQuestion($question, $choices, $default)); + } + + /** + * {@inheritdoc} + */ + public function startProgress(int $max = 0): void + { + $this->progressBar = $this->createProgress($max); + $this->progressBar->start(); + } + + /** + * {@inheritdoc} + */ + public function advanceProgress(int $step = 1): void + { + $this->getProgressBar()->advance($step); + } + + /** + * {@inheritdoc} + */ + public function finishProgress(): void + { + $this->getProgressBar()->finish(); + $this->newLine(2); + $this->progressBar = null; + } + + /** + * {@inheritdoc} + */ + public function createProgress(int $max = 0): ProgressBar + { + $progressBar = new ProgressBar($this->output, $max); + + if ('\\' !== \DIRECTORY_SEPARATOR || 'Hyper' === getenv('TERM_PROGRAM')) { + $progressBar->setEmptyBarCharacter('░'); // light shade character \u2591 + $progressBar->setProgressCharacter(''); + $progressBar->setBarCharacter('▓'); // dark shade character \u2593 + } + + return $progressBar; + } + + /** + * {@inheritdoc} + */ + public function newLine($count = 1): void + { + $this->output->write(str_repeat(PHP_EOL, $count)); + $this->bufferedOutput->write(str_repeat("\n", $count)); + } + + /** + * @param $messages + * @param int $type + */ + protected function writeln($messages, $type = OutputInterface::OUTPUT_NORMAL) + { + if (!is_iterable($messages)) { + $messages = [$messages]; + } + + foreach ($messages as $message) { + $this->output->writeln($message, $type); + $this->writeBuffer($message, true, $type); + } + } + + /** + * @param $messages + * @param bool $newline + * @param int $type + */ + protected function write($messages, $newline = false, $type = OutputInterface::OUTPUT_NORMAL) + { + if (!is_iterable($messages)) { + $messages = [$messages]; + } + + foreach ($messages as $message) { + $this->output->write($message, $newline, $type); + $this->writeBuffer($message, $newline, $type); + } + } + + /** + * @param Question $question + * @return mixed + */ + protected function askQuestion(Question $question) + { + if ($this->input->isInteractive()) { + $this->autoPrependBlock(); + } + + if (!$this->questionHelper) { + $this->questionHelper = new QuestionHelper(); + } + + $answer = $this->questionHelper->ask($this->input, $this->output, $question); + + + + + if ($this->input->isInteractive()) { + $this->newLine(); + $this->bufferedOutput->write("\n"); + } + + return $answer; + } + + /** + * Prepend a new line for an upcoming block. + * + * @return void + */ + protected function autoPrependBlock(): void + { + $chars = substr(str_replace(PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2); + + if (!isset($chars[0])) { + $this->newLine(); //empty history, so we should start with a new line. + + return; + } + //Prepend new line for each non LF chars (This means no blank line was output before) + $this->newLine(2 - substr_count($chars, "\n")); + } + + /** + * Prepend a new line for upcoming text. + * + * @return void + */ + protected function autoPrependText(): void + { + $fetched = $this->bufferedOutput->fetch(); + //Prepend new line if last char isn't EOL: + if ("\n" !== substr($fetched, -1)) { + $this->newLine(); + } + } + + /** + * @param string $message + * @param bool $newLine + * @param int $type + */ + protected function writeBuffer(string $message, bool $newLine, int $type): void + { + // We need to know if the two last chars are PHP_EOL + // Preserve the last 4 chars inserted (PHP_EOL on windows is two chars) in the history buffer + $this->bufferedOutput->write(substr($message, -4), $newLine, $type); + } + + /** + * Formats a message as a block of text. + * + * @param string|array $messages The message to write in the block + * @param string|null $type The block type (added in [] on first line) + * @param string|null $style The style to apply to the whole block + * @param string $prefix The prefix for the block + * @param bool $padding Whether to add vertical padding + * @param bool $escape Whether to escape the message + */ + protected function block($messages, $type = null, $style = null, $prefix = ' ', $padding = false, $escape = true) + { + $messages = \is_array($messages) ? array_values($messages) : [$messages]; + + if($padding) { + $this->autoPrependBlock(); + } else { + $this->autoPrependText(); + } + + $this->writeln($this->createBlock($messages, $type, $style, $prefix, $padding, $escape)); + + if($padding) { + $this->newLine(); + } + } + + /** + * Format a set of messages as a block. + * + * @param iterable $messages + * @param string|null $type + * @param string|null $style + * @param string $prefix + * @param bool $padding + * @param bool $escape + * @return array + */ + private function createBlock(iterable $messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false) + { + $indentLength = 0; + $prefixLength = Helper::strlenWithoutDecoration($this->output->getFormatter(), $prefix); + $lines = []; + + if (null !== $type) { + $type = sprintf('[%s] ', $type); + $indentLength = \strlen($type); + $lineIndentation = str_repeat(' ', $indentLength); + } + + // wrap and add newlines for each element + foreach ($messages as $key => $message) { + if ($escape) { + $message = OutputFormatter::escape($message); + } + + $lines = array_merge($lines, explode(PHP_EOL, wordwrap($message, $this->lineLength - $prefixLength - $indentLength, PHP_EOL, true))); + + if (\count($messages) > 1 && $key < \count($messages) - 1) { + $lines[] = ''; + } + } + + $firstLineIndex = 0; + if ($padding && $this->output->isDecorated()) { + $firstLineIndex = 1; + array_unshift($lines, ''); + $lines[] = ''; + } + + foreach ($lines as $i => &$line) { + if (null !== $type) { + $line = $firstLineIndex === $i ? $type.$line : $lineIndentation.$line; + } + + $line = $prefix.$line; + $line .= str_repeat(' ', $this->lineLength - Helper::strlenWithoutDecoration($this->output->getFormatter(), $line)); + + if ($style) { + $line = sprintf('<%s>%s</>', $style, $line); + } + } + + return $lines; + } + + protected function getProgressBar(): ProgressBar + { + if (!$this->progressBar) { + throw new RuntimeException('The ProgressBar is not started.'); + } + + return $this->progressBar; + } + + /** + * Calculate the column width that fits all items. + * + * @param array $items + * @return int + */ + private function _getColumnWidth(array $items): int + { + $widths = []; + foreach ($items as $item) { + $widths[] = Helper::strlen($item); + } + return $widths ? max($widths) + 2 : 0; + } +} \ No newline at end of file diff --git a/herrfristi/laravel-output-writer/src/Console/Output/OutputWriterInterface.php b/herrfristi/laravel-output-writer/src/Console/Output/OutputWriterInterface.php new file mode 100644 index 0000000..c4447eb --- /dev/null +++ b/herrfristi/laravel-output-writer/src/Console/Output/OutputWriterInterface.php @@ -0,0 +1,196 @@ +<?php +/** + * OutputWriterInterface interface source file. + * + * Contains the source code of the OutputWriterInterface interface. + * + * PHP version 7.2 + * + * @package Arc\Base\Console\Output + * @author Marvin Schreurs <m.schreurs@archimedetrading.eu> + */ + +namespace Arc\Base\Console\Output; + + +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Interface OutputWriterInterface + * + * @package Arc\Base\Console\Output + * @author Marvin Schreurs <m.schreurs@archimedetrading.eu> + */ +interface OutputWriterInterface +{ + /** + * Formats a command title. + * + * @param string $title The title text to display. + * @return void + */ + public function title(string $title): void; + + /** + * Formats a command section. + * + * @param string $heading The section heading to display. + * @return void + */ + public function section(string $heading): void; + + /** + * Formats a table. + * + * @param array $headers A list of column headers. + * @param array $rows A list of rows with cells. + * @return void + */ + public function table(array $headers, array $rows): void; + + /** + * Formats a list. + * + * @param array $items A list of items. + * @param bool $dictionary Print the list as key/value pairs. + * @return void + */ + public function list(array $items, bool $dictionary = false): void; + + /** + * Writes an unformatted line of text. + * + * @param string|array $message Message to display. + * @param string|null $style Style to apply to the message. + * @return void + */ + public function text($message, string $style = null): void; + + /** + * Formats a success message. + * + * @param string|array $message Message to display. + * @param bool $padding Render the message in a padded box. + * @return void + */ + public function success($message, bool $padding = false): void; + + /** + * Formats an error message. + * + * @param string|array $message Message to display. + * @param bool $padding Render the message in a padded box. + * @return void + */ + public function error($message, bool $padding = false): void; + + /** + * Formats a warning message. + * + * @param string|array $message Message to display. + * @param bool $padding Render the message in a padded box. + * @return void + */ + public function warning($message, bool $padding = false): void; + + /** + * Formats a comment message. + * + * @param string|array $message Message to display. + * @param bool $padding Render the message in a padded box. + * @return void + */ + public function comment($message, bool $padding = false): void; + + /** + * Formats an information message. + * + * @param string|array $message Message to display. + * @param bool $padding Render the message in a padded box. + * @return void + */ + public function info($message, bool $padding = false): void; + + /** + * Formats an exception. + * + * @param \Throwable $throwable A throwable instance to report. + * @param string|null $message Message to display with the exception. + * @return void + */ + public function exception(\Throwable $throwable, string $message = null): void; + + /** + * Asks the user for input. + * + * @param string $question Question to display. + * @param mixed|null $default Default value for input. + * @param callable|null $validator Callable used to validate input. + * @return mixed The user's input. + */ + public function ask(string $question, $default = null, $validator = null); + + /** + * Ask the user for input, the input will be hidden. + * + * @param string $question Question to display. + * @param callable|null $validator Callable used to validate input. + * @return mixed The user's input. + */ + public function password(string $question, $validator = null); + + /** + * Ask the user for confirmation (yes or no). + * + * @param string $question Question to display. + * @param bool $default Default value for input. + * @return bool The user's input. + */ + public function confirm(string $question, $default = true): bool; + + /** + * Ask the user for input using a list of choices. + * + * @param string $question Question to display. + * @param array $choices List of choices. + * @param mixed|null $default Default value for input. + * @return mixed The user's input. + */ + public function choice(string $question, array $choices, $default = null); + + /** + * Start a progressbar. + * + * @param int $max Maximum steps (0 if unknown). + */ + public function startProgress(int $max = 0): void; + + /** + * Advance the progressbar by the given number of steps. + * + * @param int $step Amount of steps to advance. + */ + public function advanceProgress(int $step = 1): void; + + /** + * Stop the progressbar. + */ + public function finishProgress(): void; + + /** + * Create a ProgressBar instance. + * + * @param int $max Maximum steps (0 if unknown). + * @return ProgressBar The ProgressBar instance. + */ + public function createProgress(int $max = 0): ProgressBar; + + /** + * Add newlines. + * + * @param int $count + * @return void + */ + public function newLine(int $count = 1): void; +} \ No newline at end of file diff --git a/herrfristi/laravel-output-writer/src/Console/Question/BaseChoiceQuestion.php b/herrfristi/laravel-output-writer/src/Console/Question/BaseChoiceQuestion.php new file mode 100644 index 0000000..09cdf24 --- /dev/null +++ b/herrfristi/laravel-output-writer/src/Console/Question/BaseChoiceQuestion.php @@ -0,0 +1,31 @@ +<?php +/** + * BaseChoiceQuestion class source file. + * + * Contains the source code of the BaseChoiceQuestion class. + * + * PHP version 7.2 + * + * @package Arc\Base\Console\Question + * @author Marvin Schreurs <m.schreurs@archimedetrading.eu> + */ + +namespace Arc\Base\Console\Question; + + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Question\ChoiceQuestion; + +/** + * Class BaseChoiceQuestion + * + * @package Arc\Base\Console\Question + * @author Marvin Schreurs <m.schreurs@archimedetrading.eu> + */ +class BaseChoiceQuestion extends ChoiceQuestion +{ + protected function isAssoc($array) + { + return true; + } +} \ No newline at end of file diff --git a/herrfristi/laravel-output-writer/src/Console/Question/BaseConfirmationQuestion.php b/herrfristi/laravel-output-writer/src/Console/Question/BaseConfirmationQuestion.php new file mode 100644 index 0000000..bf5a6a6 --- /dev/null +++ b/herrfristi/laravel-output-writer/src/Console/Question/BaseConfirmationQuestion.php @@ -0,0 +1,30 @@ +<?php +/** + * BaseConfirmationQuestion class source file. + * + * Contains the source code of the BaseConfirmationQuestion class. + * + * PHP version 7.2 + * + * @package Arc\Base\Console\Question + * @author Marvin Schreurs <m.schreurs@archimedetrading.eu> + */ + +namespace Arc\Base\Console\Question; + + +use Symfony\Component\Console\Question\ConfirmationQuestion; + +/** + * Class BaseConfirmationQuestion + * + * @package Arc\Base\Console\Question + * @author Marvin Schreurs <m.schreurs@archimedetrading.eu> + */ +class BaseConfirmationQuestion extends ConfirmationQuestion +{ + protected function isAssoc($array) + { + return true; + } +} \ No newline at end of file diff --git a/herrfristi/laravel-output-writer/src/Console/Question/BaseQuestion.php b/herrfristi/laravel-output-writer/src/Console/Question/BaseQuestion.php new file mode 100644 index 0000000..26fc542 --- /dev/null +++ b/herrfristi/laravel-output-writer/src/Console/Question/BaseQuestion.php @@ -0,0 +1,30 @@ +<?php +/** + * BaseQuestion class source file. + * + * Contains the source code of the BaseQuestion class. + * + * PHP version 7.2 + * + * @package Arc\Base\Console\Question + * @author Marvin Schreurs <m.schreurs@archimedetrading.eu> + */ + +namespace Arc\Base\Console\Question; + + +use Symfony\Component\Console\Question\Question; + +/** + * Class BaseQuestion + * + * @package Arc\Base\Console\Question + * @author Marvin Schreurs <m.schreurs@archimedetrading.eu> + */ +class BaseQuestion extends Question +{ + protected function isAssoc($array) + { + return true; + } +} \ No newline at end of file