Skip to content

Instantly share code, notes, and snippets.

@lajosbencz
Created May 1, 2016 21:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save lajosbencz/d9d5b2e696e61884ebae9429a5311666 to your computer and use it in GitHub Desktop.
Save lajosbencz/d9d5b2e696e61884ebae9429a5311666 to your computer and use it in GitHub Desktop.
Multi-line progress bar for terminal
<?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