You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

254 lines
7.2 KiB

3 years ago
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Console\Helper;
  11. use Symfony\Component\Console\Exception\InvalidArgumentException;
  12. use Symfony\Component\Console\Exception\LogicException;
  13. use Symfony\Component\Console\Output\OutputInterface;
  14. /**
  15. * @author Kevin Bond <kevinbond@gmail.com>
  16. */
  17. class ProgressIndicator
  18. {
  19. private $output;
  20. private $startTime;
  21. private $format;
  22. private $message;
  23. private $indicatorValues;
  24. private $indicatorCurrent;
  25. private $indicatorChangeInterval;
  26. private $indicatorUpdateTime;
  27. private $started = false;
  28. private static $formatters;
  29. private static $formats;
  30. /**
  31. * @param int $indicatorChangeInterval Change interval in milliseconds
  32. * @param array|null $indicatorValues Animated indicator characters
  33. */
  34. public function __construct(OutputInterface $output, string $format = null, int $indicatorChangeInterval = 100, array $indicatorValues = null)
  35. {
  36. $this->output = $output;
  37. if (null === $format) {
  38. $format = $this->determineBestFormat();
  39. }
  40. if (null === $indicatorValues) {
  41. $indicatorValues = ['-', '\\', '|', '/'];
  42. }
  43. $indicatorValues = array_values($indicatorValues);
  44. if (2 > \count($indicatorValues)) {
  45. throw new InvalidArgumentException('Must have at least 2 indicator value characters.');
  46. }
  47. $this->format = self::getFormatDefinition($format);
  48. $this->indicatorChangeInterval = $indicatorChangeInterval;
  49. $this->indicatorValues = $indicatorValues;
  50. $this->startTime = time();
  51. }
  52. /**
  53. * Sets the current indicator message.
  54. */
  55. public function setMessage(?string $message)
  56. {
  57. $this->message = $message;
  58. $this->display();
  59. }
  60. /**
  61. * Starts the indicator output.
  62. */
  63. public function start(string $message)
  64. {
  65. if ($this->started) {
  66. throw new LogicException('Progress indicator already started.');
  67. }
  68. $this->message = $message;
  69. $this->started = true;
  70. $this->startTime = time();
  71. $this->indicatorUpdateTime = $this->getCurrentTimeInMilliseconds() + $this->indicatorChangeInterval;
  72. $this->indicatorCurrent = 0;
  73. $this->display();
  74. }
  75. /**
  76. * Advances the indicator.
  77. */
  78. public function advance()
  79. {
  80. if (!$this->started) {
  81. throw new LogicException('Progress indicator has not yet been started.');
  82. }
  83. if (!$this->output->isDecorated()) {
  84. return;
  85. }
  86. $currentTime = $this->getCurrentTimeInMilliseconds();
  87. if ($currentTime < $this->indicatorUpdateTime) {
  88. return;
  89. }
  90. $this->indicatorUpdateTime = $currentTime + $this->indicatorChangeInterval;
  91. ++$this->indicatorCurrent;
  92. $this->display();
  93. }
  94. /**
  95. * Finish the indicator with message.
  96. *
  97. * @param $message
  98. */
  99. public function finish(string $message)
  100. {
  101. if (!$this->started) {
  102. throw new LogicException('Progress indicator has not yet been started.');
  103. }
  104. $this->message = $message;
  105. $this->display();
  106. $this->output->writeln('');
  107. $this->started = false;
  108. }
  109. /**
  110. * Gets the format for a given name.
  111. *
  112. * @return string|null A format string
  113. */
  114. public static function getFormatDefinition(string $name)
  115. {
  116. if (!self::$formats) {
  117. self::$formats = self::initFormats();
  118. }
  119. return self::$formats[$name] ?? null;
  120. }
  121. /**
  122. * Sets a placeholder formatter for a given name.
  123. *
  124. * This method also allow you to override an existing placeholder.
  125. */
  126. public static function setPlaceholderFormatterDefinition(string $name, callable $callable)
  127. {
  128. if (!self::$formatters) {
  129. self::$formatters = self::initPlaceholderFormatters();
  130. }
  131. self::$formatters[$name] = $callable;
  132. }
  133. /**
  134. * Gets the placeholder formatter for a given name (including the delimiter char like %).
  135. *
  136. * @return callable|null A PHP callable
  137. */
  138. public static function getPlaceholderFormatterDefinition(string $name)
  139. {
  140. if (!self::$formatters) {
  141. self::$formatters = self::initPlaceholderFormatters();
  142. }
  143. return self::$formatters[$name] ?? null;
  144. }
  145. private function display()
  146. {
  147. if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) {
  148. return;
  149. }
  150. $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) {
  151. if ($formatter = self::getPlaceholderFormatterDefinition($matches[1])) {
  152. return $formatter($this);
  153. }
  154. return $matches[0];
  155. }, $this->format ?? ''));
  156. }
  157. private function determineBestFormat(): string
  158. {
  159. switch ($this->output->getVerbosity()) {
  160. // OutputInterface::VERBOSITY_QUIET: display is disabled anyway
  161. case OutputInterface::VERBOSITY_VERBOSE:
  162. return $this->output->isDecorated() ? 'verbose' : 'verbose_no_ansi';
  163. case OutputInterface::VERBOSITY_VERY_VERBOSE:
  164. case OutputInterface::VERBOSITY_DEBUG:
  165. return $this->output->isDecorated() ? 'very_verbose' : 'very_verbose_no_ansi';
  166. default:
  167. return $this->output->isDecorated() ? 'normal' : 'normal_no_ansi';
  168. }
  169. }
  170. /**
  171. * Overwrites a previous message to the output.
  172. */
  173. private function overwrite(string $message)
  174. {
  175. if ($this->output->isDecorated()) {
  176. $this->output->write("\x0D\x1B[2K");
  177. $this->output->write($message);
  178. } else {
  179. $this->output->writeln($message);
  180. }
  181. }
  182. private function getCurrentTimeInMilliseconds(): float
  183. {
  184. return round(microtime(true) * 1000);
  185. }
  186. private static function initPlaceholderFormatters(): array
  187. {
  188. return [
  189. 'indicator' => function (self $indicator) {
  190. return $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)];
  191. },
  192. 'message' => function (self $indicator) {
  193. return $indicator->message;
  194. },
  195. 'elapsed' => function (self $indicator) {
  196. return Helper::formatTime(time() - $indicator->startTime);
  197. },
  198. 'memory' => function () {
  199. return Helper::formatMemory(memory_get_usage(true));
  200. },
  201. ];
  202. }
  203. private static function initFormats(): array
  204. {
  205. return [
  206. 'normal' => ' %indicator% %message%',
  207. 'normal_no_ansi' => ' %message%',
  208. 'verbose' => ' %indicator% %message% (%elapsed:6s%)',
  209. 'verbose_no_ansi' => ' %message% (%elapsed:6s%)',
  210. 'very_verbose' => ' %indicator% %message% (%elapsed:6s%, %memory:6s%)',
  211. 'very_verbose_no_ansi' => ' %message% (%elapsed:6s%, %memory:6s%)',
  212. ];
  213. }
  214. }