Changeset View
Changeset View
Standalone View
Standalone View
src/console/PhutilConsoleProgressBar.php
- This file was added.
| <?php | |||||
| /** | |||||
| * Show a progress bar on the console. Usage: | |||||
| * | |||||
| * // Create a progress bar, and configure the total amount of work that | |||||
| * // needs to be done. | |||||
| * $bar = id(new PhutilConsoleProgressBar()) | |||||
| * ->setTotal(count($stuff)); | |||||
| * | |||||
| * // As you complete the work, update the progress bar. | |||||
| * foreach ($stuff as $thing) { | |||||
| * do_stuff($thing); | |||||
| * $bar->update(1); | |||||
| * } | |||||
| * | |||||
| * // When complete, mark the work done to clear the bar. | |||||
| * $bar->done(); | |||||
| * | |||||
| * The progress bar attempts to account for various special cases, notably: | |||||
| * | |||||
| * - If stderr is not a TTY, the bar will not be drawn (for example, if | |||||
| * it is being piped to a log file). | |||||
| * - If the Phutil log output is enabled (usually because `--trace` was | |||||
| * specified), the bar will not be drawn. | |||||
| * - The bar will be resized to the width of the console if possible. | |||||
| * | |||||
| */ | |||||
| final class PhutilConsoleProgressBar extends Phobject { | |||||
| private $work; | |||||
| private $done; | |||||
| private $drawn; | |||||
| private $console; | |||||
| private $finished; | |||||
| private $lastUpdate; | |||||
| private $quiet = false; | |||||
| public function setConsole(PhutilConsole $console) { | |||||
| $this->console = $console; | |||||
| return $this; | |||||
| } | |||||
| private function getConsole() { | |||||
| if ($this->console) { | |||||
| return $this->console; | |||||
| } | |||||
| return PhutilConsole::getConsole(); | |||||
| } | |||||
| public function setTotal($work) { | |||||
| $this->work = $work; | |||||
| $this->redraw(); | |||||
| return $this; | |||||
| } | |||||
| public function setQuiet($quiet) { | |||||
| $this->quiet = $quiet; | |||||
| return $this; | |||||
| } | |||||
| public function update($work) { | |||||
| $this->done += $work; | |||||
| $this->redraw(); | |||||
| return $this; | |||||
| } | |||||
| private function redraw() { | |||||
| if ($this->lastUpdate + 0.1 > microtime(true)) { | |||||
| // We redrew the bar very recently; skip this update. | |||||
| return $this; | |||||
| } | |||||
| return $this->draw(); | |||||
| } | |||||
| /** | |||||
| * Explicitly redraw the bar. | |||||
| * | |||||
| * Normally, the progress bar is automatically redrawn periodically, but | |||||
| * you may want to force it to draw. | |||||
| * | |||||
| * For example, we force a draw after pre-filling the bar when resuming | |||||
| * large file uploads in `arc upload`. Otherwise, the bar may sit at 0% | |||||
| * until the first chunk completes. | |||||
| */ | |||||
| public function draw() { | |||||
| if ($this->quiet) { | |||||
| return; | |||||
| } | |||||
| if ($this->finished) { | |||||
| return; | |||||
| } | |||||
| if (!$this->work) { | |||||
| // There's no work to be done, so don't draw the bar. | |||||
| return; | |||||
| } | |||||
| $console = $this->getConsole(); | |||||
| if ($console->isErrATTY() === false) { | |||||
| return; | |||||
| } | |||||
| if ($console->isLogEnabled()) { | |||||
| return; | |||||
| } | |||||
| // Width of the stuff other than the progress bar itself. | |||||
| $chrome_width = strlen('[] 100.0% '); | |||||
| $char_width = $this->getWidth(); | |||||
| if ($char_width < $chrome_width) { | |||||
| return; | |||||
| } | |||||
| $this->lastUpdate = microtime(true); | |||||
| if (!$this->drawn) { | |||||
| $this->drawn = true; | |||||
| } | |||||
| $percent = $this->done / $this->work; | |||||
| $max_width = $char_width - $chrome_width; | |||||
| $bar_width = $percent * $max_width; | |||||
| $bar_int = floor($bar_width); | |||||
| $bar_frac = $bar_width - $bar_int; | |||||
| $frac_map = array( | |||||
| '', | |||||
| '-', | |||||
| '~', | |||||
| ); | |||||
| $frac_char = $frac_map[floor($bar_frac * count($frac_map))]; | |||||
| $pattern = "[%-{$max_width}.{$max_width}s] % 5s%%"; | |||||
| $out = sprintf( | |||||
| $pattern, | |||||
| str_repeat('=', $bar_int).$frac_char, | |||||
| sprintf('%.1f', 100 * $percent)); | |||||
| $this->eraseLine(); | |||||
| $console->writeErr('%s', $out); | |||||
| return $this; | |||||
| } | |||||
| public function done($clean_exit = true) { | |||||
| $console = $this->getConsole(); | |||||
| if ($this->drawn) { | |||||
| $this->eraseLine(); | |||||
| if ($clean_exit) { | |||||
| $console->writeErr("%s\n", pht('Done.')); | |||||
| } | |||||
| } | |||||
| $this->finished = true; | |||||
| } | |||||
| private function eraseLine() { | |||||
| $string = str_repeat(' ', $this->getWidth()); | |||||
| $console = $this->getConsole(); | |||||
| $console->writeErr("\r%s\r", $string); | |||||
| } | |||||
| private function getWidth() { | |||||
| $console = $this->getConsole(); | |||||
| $width = $console->getErrCols(); | |||||
| return min(nonempty($width, 78), 78); | |||||
| } | |||||
| public function __destruct() { | |||||
| $this->done($clean_exit = false); | |||||
| } | |||||
| } | |||||