Spinner.php 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. <?php
  2. namespace Laravel\Prompts;
  3. use Closure;
  4. use RuntimeException;
  5. class Spinner extends Prompt
  6. {
  7. /**
  8. * How long to wait between rendering each frame.
  9. */
  10. public int $interval = 100;
  11. /**
  12. * The number of times the spinner has been rendered.
  13. */
  14. public int $count = 0;
  15. /**
  16. * Whether the spinner can only be rendered once.
  17. */
  18. public bool $static = false;
  19. /**
  20. * The process ID after forking.
  21. */
  22. protected int $pid;
  23. /**
  24. * Create a new Spinner instance.
  25. */
  26. public function __construct(public string $message = '')
  27. {
  28. //
  29. }
  30. /**
  31. * Render the spinner and execute the callback.
  32. *
  33. * @template TReturn of mixed
  34. *
  35. * @param Closure(): TReturn $callback
  36. * @return TReturn
  37. */
  38. public function spin(Closure $callback): mixed
  39. {
  40. $this->capturePreviousNewLines();
  41. if (! function_exists('pcntl_fork')) {
  42. return $this->renderStatically($callback);
  43. }
  44. $originalAsync = pcntl_async_signals(true);
  45. pcntl_signal(SIGINT, fn () => exit());
  46. try {
  47. $this->hideCursor();
  48. $this->render();
  49. $this->pid = pcntl_fork();
  50. if ($this->pid === 0) {
  51. while (true) { // @phpstan-ignore-line
  52. $this->render();
  53. $this->count++;
  54. usleep($this->interval * 1000);
  55. }
  56. } else {
  57. $result = $callback();
  58. $this->resetTerminal($originalAsync);
  59. return $result;
  60. }
  61. } catch (\Throwable $e) {
  62. $this->resetTerminal($originalAsync);
  63. throw $e;
  64. }
  65. }
  66. /**
  67. * Reset the terminal.
  68. */
  69. protected function resetTerminal(bool $originalAsync): void
  70. {
  71. pcntl_async_signals($originalAsync);
  72. pcntl_signal(SIGINT, SIG_DFL);
  73. $this->eraseRenderedLines();
  74. }
  75. /**
  76. * Render a static version of the spinner.
  77. *
  78. * @template TReturn of mixed
  79. *
  80. * @param Closure(): TReturn $callback
  81. * @return TReturn
  82. */
  83. protected function renderStatically(Closure $callback): mixed
  84. {
  85. $this->static = true;
  86. try {
  87. $this->hideCursor();
  88. $this->render();
  89. $result = $callback();
  90. } finally {
  91. $this->eraseRenderedLines();
  92. }
  93. return $result;
  94. }
  95. /**
  96. * Disable prompting for input.
  97. *
  98. * @throws RuntimeException
  99. */
  100. public function prompt(): never
  101. {
  102. throw new RuntimeException('Spinner cannot be prompted.');
  103. }
  104. /**
  105. * Get the current value of the prompt.
  106. */
  107. public function value(): bool
  108. {
  109. return true;
  110. }
  111. /**
  112. * Clear the lines rendered by the spinner.
  113. */
  114. protected function eraseRenderedLines(): void
  115. {
  116. $lines = explode(PHP_EOL, $this->prevFrame);
  117. $this->moveCursor(-999, -count($lines) + 1);
  118. $this->eraseDown();
  119. }
  120. /**
  121. * Clean up after the spinner.
  122. */
  123. public function __destruct()
  124. {
  125. if (! empty($this->pid)) {
  126. posix_kill($this->pid, SIGHUP);
  127. }
  128. parent::__destruct();
  129. }
  130. }