Created
May 1, 2016 21:58
-
-
Save lajosbencz/d9d5b2e696e61884ebae9429a5311666 to your computer and use it in GitHub Desktop.
Multi-line progress bar for terminal
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class ProgressBar | |
{ | |
const NL = PHP_EOL; | |
/** Pattern used to match placeholders in message formats */ | |
const PATTERN_FORMAT = "/%(?'name'[^\\s%:]+)(:(?'format'[^%]+))?%/"; | |
/** Default format for progress update message */ | |
const FORMAT_PROGRESS = | |
"%message%" . self::NL . | |
"%elapsed:time%s %remaining:time%s" . self::NL . | |
"[%bar%]" . self::NL . | |
"%percent:0.2f%%% %done:d%/%total:d%"; | |
/** Default format for done message */ | |
const FORMAT_DONE = | |
"%total:d% items in %elapsed:time% seconds"; | |
/** Default list of progress bar characters */ | |
const DEFAULT_CHARS = "_.-+=#"; | |
#region MEMBERS | |
/** @var string */ | |
protected $_formatProgress = self::FORMAT_PROGRESS; | |
/** @var string */ | |
protected $_formatDone = self::FORMAT_DONE; | |
/** @var string */ | |
protected $_chars = self::DEFAULT_CHARS; | |
/** @var resource */ | |
protected $_stream = STDOUT; | |
/** @var int */ | |
protected $_size = 64; | |
/** @var int */ | |
protected $_total = 0; | |
/** @var int */ | |
protected $_done = 0; | |
/** @var float */ | |
protected $_ratio = 0; | |
/** @var float */ | |
protected $_start = 0; | |
/** @var float */ | |
protected $_elapsed = 0; | |
/** @var float */ | |
protected $_estimated = 0; | |
/** @var float */ | |
protected $_remaining = 0; | |
/** @var string */ | |
protected $_message = ''; | |
/** @var string */ | |
protected $_buffer = ''; | |
/** @var int */ | |
protected $_widest = 0; | |
/** @var int */ | |
protected $_lines = 0; | |
#endregion | |
#region INNER METHODS | |
protected function _write($format) | |
{ | |
if(!is_resource($this->_stream)) { | |
throw new Exception('Cannot write to output stream'); | |
} | |
$params = func_get_args(); | |
$format = array_shift($params); | |
if(count($params)>0) { | |
$format = vsprintf($format, $params); | |
} | |
fwrite($this->_stream, $format); | |
return $format; | |
} | |
protected function _line($line) | |
{ | |
$this->_write(str_pad($line, $this->_widest, '', STR_PAD_RIGHT) . self::NL); | |
} | |
protected function _formatTime($seconds) | |
{ | |
$out = ''; | |
$s = (int)($seconds % 60); | |
$seconds = floor($seconds / 60); | |
$m = (int)($seconds % 60); | |
$seconds = floor($seconds / 60); | |
$h = (int)floor($seconds % 60); | |
if($h) { | |
$out.= sprintf('%02d:', $h); | |
} | |
if($h || $m) { | |
$out.= sprintf('%02d:%02d', $m, $s); | |
} | |
else { | |
$out.= sprintf('%d', $s); | |
} | |
return $out; | |
} | |
protected function _formatBar() | |
{ | |
$bar = ''; | |
$c = strlen($this->_chars); | |
if($c<2) { | |
$chars = ' ='; | |
$c = 2; | |
} else { | |
$chars = $this->_chars; | |
} | |
$empty = $chars[0]; | |
$full = $chars[$c-1]; | |
$chars = substr($chars, 1, -1); | |
if(strlen($chars)<1) { | |
$chars = [$empty]; | |
} | |
$c = strlen($chars); | |
$p = $this->_ratio * $this->_size; | |
$f = (int)floor($p); | |
$p = (int)floor(($p - $f) * $c); | |
for($i=0; $i<$this->_size; $i++) { | |
if($i<$f) { | |
$bar.= $full; | |
} | |
elseif($i==$f) { | |
$bar.= $chars[max(min($p,$c),1)-1]; | |
} | |
else { | |
$bar.= $empty; | |
} | |
} | |
return $bar; | |
} | |
protected function _format($format) | |
{ | |
$out = ''; | |
$values = [ | |
'now' => time(), | |
'total' => $this->_total, | |
'done' => $this->_done, | |
'ratio' => $this->_ratio, | |
'elapsed' => $this->_elapsed, | |
'remaining' => $this->_remaining, | |
'estimated' => $this->_estimated, | |
'message' => $this->_message, | |
'percent' => $this->_total > 0 ? (double)$this->_done / $this->_total * 100 : 100, | |
'bar' => $this->_formatBar(), | |
]; | |
$lastOffset = 0; | |
preg_match_all(self::PATTERN_FORMAT, $format, $matches, PREG_OFFSET_CAPTURE); | |
foreach($matches['name'] as $m=>$match) { | |
$m0 = &$matches[0][$m]; | |
$offset = $m0[1]; | |
$length = strlen($m0[0]); | |
$name = $match[0]; | |
$value = ''; | |
if(isset($values[$name])) { | |
$value = $values[$name]; | |
if(is_array($matches['format'][$m])) { | |
$f = $matches['format'][$m][0]; | |
switch($f) { | |
case 'time': | |
$value = $this->_formatTime($value); | |
break; | |
default: | |
$value = sprintf('%'.$f, $value); | |
} | |
} | |
else { | |
$value = (string)$value; | |
} | |
} | |
if($name === '') { | |
$value = '%'; | |
} | |
$out.= substr($format, $lastOffset, max(0, $offset - $lastOffset)) . $value; | |
$lastOffset = $offset + $length; | |
} | |
if($lastOffset < strlen($format)) { | |
$out.= substr($format, $lastOffset); | |
} | |
return $out; | |
} | |
#endregion | |
/** | |
* Progress constructor. | |
* @param bool $start (optional) | |
* @param int $total (optional) | |
* @param resource $stream (optional) | |
*/ | |
public function __construct($start=true, $total=null, $stream=null) | |
{ | |
if($total>0) { | |
$this->setTotal($total); | |
} | |
if(is_resource($stream)) { | |
$this->setStream($stream); | |
} | |
if($start) { | |
$this->start(); | |
} | |
} | |
public function reset() | |
{ | |
$this->_total = | |
$this->_done = | |
$this->_ratio = | |
$this->_widest = | |
$this->_lines = | |
$this->_elapsed = 0; | |
$this->_estimated = | |
$this->_remaining = 1; | |
$this->_buffer = | |
$this->_message = ''; | |
return $this; | |
} | |
/** | |
* @param int $n (optional) | |
* @return $this | |
*/ | |
public function cursorUp($n=null) { | |
if(!$n) { | |
$n = $this->_lines; | |
} | |
$this->_write(chr(27) . "[" . $n . "A" . chr(27) . "[0G"); | |
return $this; | |
} | |
public function clear($reset=false) | |
{ | |
$this->cursorUp(); | |
for($i=0; $i++<$this->_lines;) { | |
$this->_line(str_repeat(' ', $this->_widest)); | |
} | |
$this->cursorUp(); | |
if($reset) { | |
$this->reset(); | |
} | |
return $this; | |
} | |
public function start($total=0) | |
{ | |
if($total>0) { | |
$this->setTotal($total); | |
} | |
$this->_start = microtime(true); | |
return $this; | |
} | |
public function update($done=null) | |
{ | |
if($done>0) { | |
$this->setDone($done); | |
} | |
$this->_ratio = $this->_total ? (double)$this->_done / $this->_total : 0; | |
$this->_elapsed = microtime(true) - $this->_start; | |
$this->_estimated = $this->_ratio ? (double)$this->_elapsed / $this->_ratio : 1; | |
$this->_remaining = $this->_estimated - $this->_elapsed; | |
if($this->_done > 1) { | |
$this->cursorUp(); | |
} | |
$this->_buffer = trim($this->_format($this->_formatProgress)); | |
$lines = explode(self::NL, $this->_buffer); | |
$this->_lines = count($lines); | |
foreach($lines as $line) { | |
$this->_widest = max($this->_widest, strlen(rtrim($line," \r\n"))); | |
} | |
$this->_write($this->_buffer.self::NL); | |
if($this->_done >= $this->_total) { | |
$this->done(); | |
} | |
return $this; | |
} | |
public function increment($i=1, $message=null) | |
{ | |
$i = max(1, $i); | |
if($message) | |
{ | |
$this->setMessage($message); | |
} | |
$this->update($this->_done + $i); | |
return $this; | |
} | |
public function advance($message=null) | |
{ | |
$this->increment(1, $message); | |
return $this; | |
} | |
public function stop($clear=true) | |
{ | |
if($clear) { | |
$this->clear(); | |
} | |
$this->reset(); | |
return $this; | |
} | |
public function done() | |
{ | |
$this->clear(); | |
$this->_write($this->_format($this->_formatDone).self::NL); | |
return $this; | |
} | |
#region SETTERS | |
/** | |
* @param string $chars | |
* @return Progress | |
*/ | |
public function setChars($chars) | |
{ | |
$this->_chars = $chars; | |
return $this; | |
} | |
/** | |
* @param string $formatProgress | |
* @return Progress | |
*/ | |
public function setFormatProgress($formatProgress) | |
{ | |
$this->_formatProgress = $formatProgress; | |
return $this; | |
} | |
/** | |
* @param string $formatDone | |
* @return Progress | |
*/ | |
public function setFormatDone($formatDone) | |
{ | |
$this->_formatDone = $formatDone; | |
return $this; | |
} | |
/** | |
* @param resource $stream | |
* @param bool $clear (optional) | |
* @return $this | |
*/ | |
public function setStream($stream, $clear=false) | |
{ | |
if($clear) { | |
$this->clear(); | |
} | |
$this->_stream = $stream; | |
return $this; | |
} | |
/** | |
* @param int $size | |
* @return $this | |
*/ | |
public function setSize($size) | |
{ | |
$this->_size = $size; | |
return $this; | |
} | |
/** | |
* @param int $total | |
* @return $this | |
*/ | |
public function setTotal($total) | |
{ | |
$this->_total = $total; | |
return $this; | |
} | |
/** | |
* @param int $done | |
* @return $this | |
*/ | |
public function setDone($done) | |
{ | |
$this->_done = $done; | |
return $this; | |
} | |
/** | |
* @param string $message | |
* @return $this | |
*/ | |
public function setMessage($message) | |
{ | |
$this->_message = $message; | |
return $this; | |
} | |
#endregion | |
#region GETTERS | |
/** | |
* @return string | |
*/ | |
public function getChars() | |
{ | |
return $this->_chars; | |
} | |
/** | |
* @return resource | |
*/ | |
public function getStream() | |
{ | |
return $this->_stream; | |
} | |
/** | |
* @return int | |
*/ | |
public function getSize() | |
{ | |
return $this->_size; | |
} | |
/** | |
* @return int | |
*/ | |
public function getTotal() | |
{ | |
return $this->_total; | |
} | |
/** | |
* @return int | |
*/ | |
public function getDone() | |
{ | |
return $this->_done; | |
} | |
/** | |
* @return float | |
*/ | |
public function getRatio() | |
{ | |
return $this->_ratio; | |
} | |
/** | |
* @return float | |
*/ | |
public function getStart() | |
{ | |
return $this->_start; | |
} | |
/** | |
* @return float | |
*/ | |
public function getElapsed() | |
{ | |
return $this->_elapsed; | |
} | |
/** | |
* @return float | |
*/ | |
public function getEstimated() | |
{ | |
return $this->_estimated; | |
} | |
/** | |
* @return float | |
*/ | |
public function getRemaining() | |
{ | |
return $this->_remaining; | |
} | |
/** | |
* @return string | |
*/ | |
public function getMessage() | |
{ | |
return $this->_message; | |
} | |
/** | |
* @return int | |
*/ | |
public function getWidest() | |
{ | |
return $this->_widest; | |
} | |
/** | |
* @return string | |
*/ | |
public function getFormatProgress() | |
{ | |
return $this->_formatProgress; | |
} | |
/** | |
* @return string | |
*/ | |
public function getFormatDone() | |
{ | |
return $this->_formatDone; | |
} | |
#endregion | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment